From 6de7f76e25ceaea4cc2335cb57582de44361b4d0 Mon Sep 17 00:00:00 2001 From: byidi Date: Mon, 16 Jan 2023 10:43:23 +0100 Subject: [PATCH] check-guide --- .github/workflows/ci.yml | 33 +++ docs/.env | 24 ++ docs/.env.test | 6 + docs/.gitignore | 20 ++ docs/Makefile | 6 + docs/bin/pdg | 12 +- docs/composer.json | 41 +++- docs/config/bundles.php | 13 + docs/config/packages/api_platform.yaml | 10 + docs/config/packages/cache.yaml | 19 ++ docs/config/packages/doctrine.yaml | 43 ++++ docs/config/packages/doctrine_migrations.yaml | 6 + docs/config/packages/framework.yaml | 25 ++ docs/config/packages/nelmio_cors.yaml | 10 + docs/config/packages/routing.yaml | 12 + docs/config/packages/security.yaml | 39 +++ docs/config/packages/twig.yaml | 6 + docs/config/packages/validator.yaml | 13 + docs/config/preload.php | 5 + docs/config/routes.yaml | 0 docs/config/routes/api_platform.yaml | 4 + docs/config/routes/framework.yaml | 4 + docs/config/services.php | 19 -- docs/config/services.yaml | 26 ++ docs/guide/000-Declare-a-Resource.php | 66 ++--- docs/guide/001-Provide-the-Resource-state.php | 17 +- docs/guide/doctrine-guide.php | 89 +++++++ docs/migrations/.gitignore | 0 docs/phpunit.xml.dist | 42 ++++ docs/src/Command/CheckGuideCommand.php | 227 ++++++++++++++++++ docs/src/Command/PhpUnitCommand.php | 19 ++ docs/src/Command/TestGuideCommand.php | 70 ++++++ docs/src/DataFixtures/AppFixtures.php | 17 ++ docs/src/Entity/.gitignore | 0 docs/src/Kernel.php | 149 +++++++++++- .../StaticResourceNameCollectionFactory.php | 35 +++ docs/src/Repository/.gitignore | 0 docs/symfony.lock | 193 +++++++++++++++ docs/templates/base.html.twig | 19 ++ docs/tests/bootstrap.php | 11 + 40 files changed, 1280 insertions(+), 70 deletions(-) create mode 100644 docs/.env create mode 100644 docs/.env.test create mode 100644 docs/config/bundles.php create mode 100644 docs/config/packages/api_platform.yaml create mode 100644 docs/config/packages/cache.yaml create mode 100644 docs/config/packages/doctrine.yaml create mode 100644 docs/config/packages/doctrine_migrations.yaml create mode 100644 docs/config/packages/framework.yaml create mode 100644 docs/config/packages/nelmio_cors.yaml create mode 100644 docs/config/packages/routing.yaml create mode 100644 docs/config/packages/security.yaml create mode 100644 docs/config/packages/twig.yaml create mode 100644 docs/config/packages/validator.yaml create mode 100644 docs/config/preload.php create mode 100644 docs/config/routes.yaml create mode 100644 docs/config/routes/api_platform.yaml create mode 100644 docs/config/routes/framework.yaml delete mode 100644 docs/config/services.php create mode 100644 docs/config/services.yaml create mode 100644 docs/guide/doctrine-guide.php create mode 100644 docs/migrations/.gitignore create mode 100644 docs/phpunit.xml.dist create mode 100644 docs/src/Command/CheckGuideCommand.php create mode 100644 docs/src/Command/PhpUnitCommand.php create mode 100644 docs/src/Command/TestGuideCommand.php create mode 100644 docs/src/DataFixtures/AppFixtures.php create mode 100644 docs/src/Entity/.gitignore create mode 100644 docs/src/Metadata/Resource/Factory/StaticResourceNameCollectionFactory.php create mode 100644 docs/src/Repository/.gitignore create mode 100644 docs/symfony.lock create mode 100644 docs/templates/base.html.twig create mode 100644 docs/tests/bootstrap.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba86fef062c..1225665bdbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -728,3 +728,36 @@ jobs: run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction + + checkguide: + runs-on: ubuntu-latest + strategy: + matrix: + php: + - '8.1' + fail-fast: false + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: intl, bcmath, curl, openssl, mbstring + ini-values: memory_limit=-1 + tools: pecl, composer + coverage: none + - name: Get composer cache directory + id: composercache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Update project dependencies + run: cd docs && composer update --no-interaction --no-progress --ansi + - name: Run check command + run: cd docs && make check \ No newline at end of file diff --git a/docs/.env b/docs/.env new file mode 100644 index 00000000000..c4ac5595a83 --- /dev/null +++ b/docs/.env @@ -0,0 +1,24 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# https://symfony.com/doc/current/configuration/secrets.html +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration + +###> symfony/framework-bundle ### +APP_ENV=dev +APP_SECRET=ab0da85135bd245202dd6c8de5b90aa9 +###< symfony/framework-bundle ### + +###> nelmio/cors-bundle ### +CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' +###< nelmio/cors-bundle ### diff --git a/docs/.env.test b/docs/.env.test new file mode 100644 index 00000000000..9e7162f0b01 --- /dev/null +++ b/docs/.env.test @@ -0,0 +1,6 @@ +# define your env variables for the test env here +KERNEL_CLASS='App\Kernel' +APP_SECRET='$ecretf0rt3st' +SYMFONY_DEPRECATIONS_HELPER=999999 +PANTHER_APP_ENV=panther +PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots diff --git a/docs/.gitignore b/docs/.gitignore index 2b2fdb4faf8..b3ffc8fa426 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -7,3 +7,23 @@ node_modules composer.lock vendor var + +###> symfony/framework-bundle ### +/.env.local +/.env.local.php +/.env.*.local +/config/secrets/prod/prod.decrypt.private.php +/public/bundles/ +/var/ +/vendor/ +###< symfony/framework-bundle ### + +###> phpunit/phpunit ### +/phpunit.xml +.phpunit.result.cache +###< phpunit/phpunit ### + +###> symfony/phpunit-bridge ### +.phpunit.result.cache +/phpunit.xml +###< symfony/phpunit-bridge ### diff --git a/docs/Makefile b/docs/Makefile index 6b74cd2a562..520dc716c1b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -8,6 +8,12 @@ guide: ./bin/pdg pdg:guide "$$d" > "pages/guide/$$name.mdx"; \ done +check: + for d in ./guide/*.php; do \ + name=$$(basename $$d .php); \ + ./bin/pdg pdg:check:guide "$$d"; \ + done + index: ./bin/pdg pdg:index > "pages/sidebar.mdx" diff --git a/docs/bin/pdg b/docs/bin/pdg index 3db9285dbbe..1503c5e1b35 100755 --- a/docs/bin/pdg +++ b/docs/bin/pdg @@ -4,12 +4,14 @@ use PDG\Kernel; use Symfony\Bundle\FrameworkBundle\Console\Application; -require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; +if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) { + throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".'); +} -return function (array $context) { - $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); +require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; - // returning an "Application" makes the Runtime run a Console - // application instead of the HTTP Kernel +return function (array $context, string $guide = '') { + $kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG'], $guide); + return new Application($kernel); }; diff --git a/docs/composer.json b/docs/composer.json index 641bc13feed..09eccd6941c 100644 --- a/docs/composer.json +++ b/docs/composer.json @@ -11,24 +11,57 @@ "psr-4": { "PDG\\": "src/", "ApiPlatform\\": "../src" - } + }, + "files": [ + "src/Command/CheckGuideCommand.php", + "src/Command/PhpUnitCommand.php", + "src/Command/TestGuideCommand.php" + ] }, "require-dev": { "symfony/dom-crawler": "^6.2", "phpstan/phpdoc-parser": "^1.15", "symfony/serializer": "^6.2", - "nikic/php-parser": "^4.15" + "nikic/php-parser": "^4.15", + "dama/doctrine-test-bundle": "^7.1", + "justinrainbow/json-schema": "^5.2", + "phpunit/phpunit": "^9.5", + "symfony/browser-kit": "^6.2", + "symfony/css-selector": "^6.2", + "symfony/http-client": "^6.2", + "symfony/phpunit-bridge": "^6.2" }, "require": { "symfony/runtime": "^6.2", "symfony/console": "^6.2", "symfony/http-kernel": "^6.2", "symfony/framework-bundle": "^6.2", - "spatie/yaml-front-matter": "^2.0" + "spatie/yaml-front-matter": "^2.0", + "symfony/property-info": "^6.2", + "symfony/property-access": "^6.2", + "symfony/flex": "^2.2", + "nelmio/cors-bundle": "^2.2", + "symfony/security-bundle": "^6.2", + "willdurand/negotiation": "^3.1", + "symfony/twig-bundle": "^6.2", + "symfony/web-link": "^6.2", + "doctrine/annotations": "^2.0", + "doctrine/doctrine-bundle": "^2.8", + "doctrine/doctrine-migrations-bundle": "^3.2", + "doctrine/orm": "^2.14", + "doctrine/doctrine-fixtures-bundle": "^3.4", + "symfony/validator": "^6.2" }, "config": { "allow-plugins": { - "symfony/runtime": true + "symfony/runtime": true, + "symfony/flex": true + } + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" } } } diff --git a/docs/config/bundles.php b/docs/config/bundles.php new file mode 100644 index 00000000000..d5cca32509a --- /dev/null +++ b/docs/config/bundles.php @@ -0,0 +1,13 @@ + ['all' => true], + ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], + Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], + Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], + DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], +]; diff --git a/docs/config/packages/api_platform.yaml b/docs/config/packages/api_platform.yaml new file mode 100644 index 00000000000..6319b41bb8d --- /dev/null +++ b/docs/config/packages/api_platform.yaml @@ -0,0 +1,10 @@ +api_platform: + formats: + json: [ 'application/json' ] + jsonld: [ 'application/ld+json' ] + + # mapping: + # paths: + # - '%kernel.project_dir%/src' + # doctrine: + # enabled: true diff --git a/docs/config/packages/cache.yaml b/docs/config/packages/cache.yaml new file mode 100644 index 00000000000..6899b72003f --- /dev/null +++ b/docs/config/packages/cache.yaml @@ -0,0 +1,19 @@ +framework: + cache: + # Unique name of your app: used to compute stable namespaces for cache keys. + #prefix_seed: your_vendor_name/app_name + + # The "app" cache stores to the filesystem by default. + # The data in this cache should persist between deploys. + # Other options include: + + # Redis + #app: cache.adapter.redis + #default_redis_provider: redis://localhost + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + #pools: + #my.dedicated.cache: null diff --git a/docs/config/packages/doctrine.yaml b/docs/config/packages/doctrine.yaml new file mode 100644 index 00000000000..db34a342314 --- /dev/null +++ b/docs/config/packages/doctrine.yaml @@ -0,0 +1,43 @@ +doctrine: + dbal: + url: '%database_url%' + + # IMPORTANT: You MUST configure your server version, + # either here or in the DATABASE_URL env var (see .env file) + #server_version: '14' + orm: + auto_generate_proxy_classes: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + App: + is_bundle: false + dir: '%kernel.project_dir%/src' + prefix: 'App\Entity' + alias: App + +when@test: + doctrine: + dbal: + # "TEST_TOKEN" is typically set by ParaTest + dbname_suffix: '_test%env(default::TEST_TOKEN)%' + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' + query_cache_driver: + type: pool + pool: doctrine.system_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/docs/config/packages/doctrine_migrations.yaml b/docs/config/packages/doctrine_migrations.yaml new file mode 100644 index 00000000000..972c134029c --- /dev/null +++ b/docs/config/packages/doctrine_migrations.yaml @@ -0,0 +1,6 @@ +doctrine_migrations: + migrations_paths: + # namespace is arbitrary but should be different from App\Migrations + # as migrations classes should NOT be autoloaded + 'DoctrineMigrations': '%kernel.project_dir%/src' + enable_profiler: false diff --git a/docs/config/packages/framework.yaml b/docs/config/packages/framework.yaml new file mode 100644 index 00000000000..6d85c2957cb --- /dev/null +++ b/docs/config/packages/framework.yaml @@ -0,0 +1,25 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + secret: '%env(APP_SECRET)%' + #csrf_protection: true + http_method_override: false + handle_all_throwables: true + + # Enables session support. Note that the session will ONLY be started if you read or write from it. + # Remove or comment this section to explicitly disable session support. + session: + handler_id: null + cookie_secure: auto + cookie_samesite: lax + storage_factory_id: session.storage.factory.native + + #esi: true + #fragments: true + php_errors: + log: true + +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/docs/config/packages/nelmio_cors.yaml b/docs/config/packages/nelmio_cors.yaml new file mode 100644 index 00000000000..08a7659d1a1 --- /dev/null +++ b/docs/config/packages/nelmio_cors.yaml @@ -0,0 +1,10 @@ +nelmio_cors: + defaults: + origin_regex: true + allow_origin: ['^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'] + allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] + allow_headers: ['Content-Type', 'Authorization'] + expose_headers: ['Link'] + max_age: 3600 + paths: + '^/': null diff --git a/docs/config/packages/routing.yaml b/docs/config/packages/routing.yaml new file mode 100644 index 00000000000..4b766ce57ff --- /dev/null +++ b/docs/config/packages/routing.yaml @@ -0,0 +1,12 @@ +framework: + router: + utf8: true + + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands + #default_uri: http://localhost + +when@prod: + framework: + router: + strict_requirements: null diff --git a/docs/config/packages/security.yaml b/docs/config/packages/security.yaml new file mode 100644 index 00000000000..367af25a56d --- /dev/null +++ b/docs/config/packages/security.yaml @@ -0,0 +1,39 @@ +security: + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: + users_in_memory: { memory: null } + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true + provider: users_in_memory + + # activate different ways to authenticate + # https://symfony.com/doc/current/security.html#the-firewall + + # https://symfony.com/doc/current/security/impersonating_user.html + # switch_user: true + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_USER } + +when@test: + security: + password_hashers: + # By default, password hashers are resource intensive and take time. This is + # important to generate secure password hashes. In tests however, secure hashes + # are not important, waste resources and increase test times. The following + # reduces the work factor to the lowest possible values. + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon diff --git a/docs/config/packages/twig.yaml b/docs/config/packages/twig.yaml new file mode 100644 index 00000000000..f9f4cc539ba --- /dev/null +++ b/docs/config/packages/twig.yaml @@ -0,0 +1,6 @@ +twig: + default_path: '%kernel.project_dir%/templates' + +when@test: + twig: + strict_variables: true diff --git a/docs/config/packages/validator.yaml b/docs/config/packages/validator.yaml new file mode 100644 index 00000000000..0201281d3cb --- /dev/null +++ b/docs/config/packages/validator.yaml @@ -0,0 +1,13 @@ +framework: + validation: + email_validation_mode: html5 + + # Enables validator auto-mapping support. + # For instance, basic validation constraints will be inferred from Doctrine's metadata. + #auto_mapping: + # App\Entity\: [] + +when@test: + framework: + validation: + not_compromised_password: false diff --git a/docs/config/preload.php b/docs/config/preload.php new file mode 100644 index 00000000000..5ebcdb2153c --- /dev/null +++ b/docs/config/preload.php @@ -0,0 +1,5 @@ +services() - ->defaults() - ->autowire() // Automatically injects dependencies in your services. - ->autoconfigure() // Automatically registers your services as commands, event subscribers, etc. - ; - - // makes classes in src/ available to be used as services - // this creates a service per class whose id is the fully-qualified class name - $services->load('PDG\\', '../src/') - ->exclude('../src/{DependencyInjection,Entity,Kernel.php, config.php}'); - - // order is important in this file because service definitions - // always *replace* previous ones; add your own service configuration below -}; diff --git a/docs/config/services.yaml b/docs/config/services.yaml new file mode 100644 index 00000000000..48547c63eb9 --- /dev/null +++ b/docs/config/services.yaml @@ -0,0 +1,26 @@ +# This file is the entry point to configure your own services. +# Files in the packages/ subdirectory configure your dependencies. + +# Put parameters here that don't need to change on each machine where the app is deployed +# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration +parameters: + database_url: 'sqlite:///%kernel.project_dir%/var/data.db' + +services: + # default configuration for services in *this* file + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + + # # makes classes in src/ available to be used as services + # # this creates a service per class whose id is the fully-qualified class name + # App\: + # resource: '../src/' + # exclude: + # - '../src/DependencyInjection/' + # - '../src/Entity/' + # - '../src/Kernel.php' + # - '../src/guide.php' + + # add more service definitions when explicit configuration is needed + # please note that last definitions always *replace* previous ones diff --git a/docs/guide/000-Declare-a-Resource.php b/docs/guide/000-Declare-a-Resource.php index 0deefa9259e..1182583c1e4 100644 --- a/docs/guide/000-Declare-a-Resource.php +++ b/docs/guide/000-Declare-a-Resource.php @@ -8,39 +8,39 @@ // # Declare a Resource // This class represents an API resource -namespace App\ApiResource; +namespace App\ApiResource{ + use ApiPlatform\Metadata\Get; + use ApiPlatform\Metadata\GetCollection; + + #[GetCollection(provider: BlogPostProvider::class)] + class Book{ + public ?int $id = null; + } +} + +namespace App\Provider{ + use App\ApiResource\Book; + use ApiPlatform\Metadata\Operation; + use ApiPlatform\State\ProviderInterface; + + class Provider implements ProviderInterface{ + /** + * {@inheritDoc} + */ + public function provide(Operation $operation, array $uriVariables = [], array $context = []): array|object|null + { + $book = new Book(); + $book->id = 42; + return [$book]; + } + } +} -// The `#[ApiResource]` attribute registers this class as an HTTP resource. -use ApiPlatform\Metadata\ApiResource; -// These are the list of HTTP operations we use to declare a "CRUD" (Create, Read, Update, Delete). -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Post; -use ApiPlatform\Metadata\Patch; -use ApiPlatform\Metadata\Delete; -use ApiPlatform\Validator\Exception\ValidationException; +namespace App\Playground { + use Symfony\Component\HttpFoundation\Request; -// Each resource has its set of Operations. -// Note that the uriTemplate may use the `id` variable which is our unique -// identifier on this `Book`. -#[ApiResource( - operations: [ - new Get(uriTemplate: '/books/{id}'), - // The GetCollection operation returns a list of Books. - new GetCollection(uriTemplate: '/books'), - new Post(uriTemplate: '/books'), - new Patch(uriTemplate: '/books/{id}'), - new Delete(uriTemplate: '/books/{id}'), - ], - // This is a configuration that is shared accross every operations. More details are available at [ApiResource::exceptionToStatus](/reference/Metadata/ApiResource#exceptionToStatus). - exceptionToStatus: [ - ValidationException::class => 422 - ] -)] -// If a property named `id` is found it is the property used in your URI template -// we recommend to use public properties to declare API resources. -class Book -{ - public string $id; + function request(): Request + { + return Request::create('/docs.json'); + } } -// Select the next example to see how to hook a persistence layer. diff --git a/docs/guide/001-Provide-the-Resource-state.php b/docs/guide/001-Provide-the-Resource-state.php index abdc6626536..c058ccadf21 100644 --- a/docs/guide/001-Provide-the-Resource-state.php +++ b/docs/guide/001-Provide-the-Resource-state.php @@ -9,7 +9,7 @@ // # Provide the Resource State // Our model is the same then in the previous guide ([Declare a Resource](./declare-a-resource). API Platform will declare // CRUD operations if we don't declare them. -namespace App\ApiResource { +namespace App\Entity { use ApiPlatform\Metadata\ApiResource; use App\State\BookProvider; @@ -18,6 +18,10 @@ class Book { public string $id; + + public function getId(){ + return $this->id; + } } } @@ -25,7 +29,7 @@ class Book use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; - use App\ApiResource\Book; + use App\Entity\Book; // The BookProvider is where we retrieve the data in our persistence layer. // In this provider we choose to handle the retrieval of a single Book but also a list of Books. @@ -48,4 +52,13 @@ public function provide(Operation $operation, array $uriVariables = [], array $c } } +namespace App\Playground { + use Symfony\Component\HttpFoundation\Request; + + function request(): Request + { + return Request::create('/docs.json'); + } +} + diff --git a/docs/guide/doctrine-guide.php b/docs/guide/doctrine-guide.php new file mode 100644 index 00000000000..a04034dc2a5 --- /dev/null +++ b/docs/guide/doctrine-guide.php @@ -0,0 +1,89 @@ + + */ + #[ApiResource] + #[ORM\Entity] + class Book + { + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private $id; + #[ORM\Column] + public $name; + #[ORM\Column(unique: true)] + public $isbn; + + public function getId() + { + return $this->id; + } + } +} + +namespace App\Playground { + use App\Kernel; + use Symfony\Component\HttpFoundation\Request; + + function request(): Request + { + $body = [ + 'name' => 'bookToto', + 'isbn' => 'abcd' + ]; + return Request::create('/books.jsonld', 'POST',[], [], [], ['CONTENT_TYPE' => 'application/json'], json_encode($body)); + } + + function setup(Kernel $kernel): void + { + $kernel->executeMigrations(); + } +} + +namespace DoctrineMigrations { + + use Doctrine\DBAL\Schema\Schema; + use Doctrine\Migrations\AbstractMigration; + + final class Migration extends AbstractMigration + { + public function up(Schema $schema): void + { + $this->addSql('CREATE TABLE book (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, isbn VARCHAR(255) NOT NULL)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_CBE5A331CC1CF4E6 ON book (isbn)'); + } + } +} + +namespace App\Fixtures { + use App\Entity\Book; + use Doctrine\Bundle\FixturesBundle\Fixture; + use Doctrine\Persistence\ObjectManager; + use Zenstruck\Foundry\AnonymousFactory; + use function Zenstruck\Foundry\faker; + + final class BookFixtures extends Fixture + { + public function load(ObjectManager $manager): void + { + $factory = AnonymousFactory::new(Book::class); + $factory->many(20)->create(static function (int $i): array { + return [ + 'name' => faker()->name, + 'isbn' => faker()->isbn10() + ]; + }); + } + } +} \ No newline at end of file diff --git a/docs/migrations/.gitignore b/docs/migrations/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/phpunit.xml.dist b/docs/phpunit.xml.dist new file mode 100644 index 00000000000..af3f14747bd --- /dev/null +++ b/docs/phpunit.xml.dist @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + tests + + + + + + src + + + + + + + + + + diff --git a/docs/src/Command/CheckGuideCommand.php b/docs/src/Command/CheckGuideCommand.php new file mode 100644 index 00000000000..9d344b75a97 --- /dev/null +++ b/docs/src/Command/CheckGuideCommand.php @@ -0,0 +1,227 @@ +addArgument( + 'guide', + InputArgument::REQUIRED, + 'the path to the guide to test' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $stderr = $io->getErrorStyle(); + $guide = $input->getArgument('guide'); + $kernel = new Kernel('dev', true, $this->getGuideName($guide)); + + $stderr->title(\sprintf('Checking guide %s', $this->getGuideName($guide))); + + if (\file_exists($kernel->getCacheDir()) && !$this->deleteCacheDir($kernel->getCacheDir())) { + $stderr->warning('Can not delete cache dir.'); + } + + if (\file_exists($kernel->getDBDir()) && !$this->deleteCacheDir($kernel->getDBDir())) { + $stderr->warning('Can not delete cache dir.'); + } + + $handle = \fopen($guide, 'r'); + if (!$handle) { + $stderr->error('Error opening guide.'); + + return Command::INVALID; + } + + $frontMatterOpen = false; + $headers = $this->getGuideHeaders($guide); + + if ( + \count($headers) === 0 + ) { + $stderr->error('No headers detected.'); + + return Command::FAILURE; + } + + $missingHeaders = \array_diff(self::REQUIRED_HEADERS, \array_keys($headers)); + if (\count($missingHeaders) > 0) { + $stderr->error(\sprintf( + 'Missing required headers: %s', + \implode(', ', $missingHeaders) + )); + + return Command::FAILURE; + } + $stderr->info('Headers ok.'); + + try { + require $guide; + } catch (\Throwable $e) { + $stderr->error(\sprintf( + 'Invalid code: %s.', + $e->getMessage() + )); + + return Command::FAILURE; + } + + if ($headers['executable'] !== 'true') { + $stderr->warning(\sprintf( + 'Guide %s is not set to be excecutable, skipping.', + $this->getGuideName($guide), + )); + + return Command::SUCCESS; + } + $stderr->info('Is executable.'); + + $migrationClasses = $kernel->getDeclaredClassesForNamespace('DoctrineMigrations'); + + if (\count($migrationClasses) > 0) { + try { + $kernel->executeMigrations(); + } catch (\Throwable $e) { + $stderr->error(\sprintf( + 'Migration error(s): %s.', + $e->getMessage() + )); + + return Command::FAILURE; + } + } + + $stderr->info(\sprintf( + '%s.', + \count($migrationClasses) > 0 + ? 'Migrations ok' : 'No migrations' + )); + + if (!\function_exists('\App\Playground\request')) { + $stderr->error('No request function.'); + + return Command::FAILURE; + } + + $response = $kernel->handle(request()); + if ($response->getStatusCode() >= 400) { + $stderr->error(\sprintf( + 'Request failed: %d.', + $response->getStatusCode() + )); + + return Command::FAILURE; + } + $stderr->info('Request ok.'); + + $testClasses = $kernel->getDeclaredClassesForNamespace('App\Tests'); + if (\count($testClasses) > 0) { + $testCommand = new TestGuideCommand(); + $testResult = $testCommand->run(new ArrayInput([ + 'guide' => $guide, + ]), $output); + + if ($testResult !== TestRunner::SUCCESS_EXIT) { + return Command::FAILURE; + } + } else { + $stderr->warning('No test found in this guide.'); + } + + $stderr->success('All good.'); + + return Command::SUCCESS; + } + + private function getGuideName(string $guide): string + { + $expl = \explode('/', $guide); + + return \str_replace('.php', '', \end($expl)); + } + + private function deleteCacheDir(string $directory): bool + { + if (!\file_exists($directory)) { + return true; + } + + if (!\is_dir($directory)) { + return \unlink($directory); + } + + foreach (\scandir($directory) as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + if (!$this->deleteCacheDir($directory.\DIRECTORY_SEPARATOR.$item)) { + return false; + } + } + + return \rmdir($directory); + } + + /** + * @return string[] + */ + private function getGuideHeaders(string $guide): array + { + $handle = \fopen($guide, 'r'); + $frontMatterOpen = false; + $headers = []; + while (($line = \fgets($handle)) !== false) { + if ( + !\trim($line) + || !\preg_match(self::COMMENTARY_REGEX, $line) + ) { + continue; + } + + $text = \preg_replace(self::COMMENTARY_REGEX, '', $line); + + if (\trim($text) === '---') { + $frontMatterOpen = !$frontMatterOpen; + if (!$frontMatterOpen) { + break; + } + continue; + } + + if ($frontMatterOpen) { + $header = \explode(':', $text); + $headers[\trim($header[0])] = \trim($header[1]); + } + } + + return $headers; + } +} diff --git a/docs/src/Command/PhpUnitCommand.php b/docs/src/Command/PhpUnitCommand.php new file mode 100644 index 00000000000..c63ff061168 --- /dev/null +++ b/docs/src/Command/PhpUnitCommand.php @@ -0,0 +1,19 @@ +arguments['test'] = self::$suite; + } +} \ No newline at end of file diff --git a/docs/src/Command/TestGuideCommand.php b/docs/src/Command/TestGuideCommand.php new file mode 100644 index 00000000000..c615f390bcb --- /dev/null +++ b/docs/src/Command/TestGuideCommand.php @@ -0,0 +1,70 @@ +addArgument( + 'guide', + InputArgument::REQUIRED, + 'the path to the guide to test' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $style = new SymfonyStyle($input, $output); + $guide = $input->getArgument('guide'); + $style->info('Testing guide: ' . $guide); + + $suite = new TestSuite(); + + require_once($guide); + + $testClasses = $this->getDeclaredClassesForNamesPace('App\Tests'); + $migrationClasses = $this->getDeclaredClassesForNamesPace('DoctrineMigrations'); + + if ($migrationClasses) { + $app = new Kernel('test', true, $this->getGuideName($guide)); + $app->executeMigrations(); + } + + foreach ($testClasses as $testClass) { + $suite->addTestSuite($testClass); + } + + PhpUnitCommand::setSuite($suite); + return PhpUnitCommand::main(false); + } + + /** + * @return array|string[] + */ + private function getDeclaredClassesForNamesPace(string $namespace): array + { + return array_filter(get_declared_classes(), static function (string $class) use ($namespace): bool { + return str_starts_with($class, $namespace); + }); + } + + private function getGuideName(string $guide): string + { + $expl = explode('/', $guide); + return str_replace('.php', '', end($expl)); + } +} diff --git a/docs/src/DataFixtures/AppFixtures.php b/docs/src/DataFixtures/AppFixtures.php new file mode 100644 index 00000000000..987f6fe955c --- /dev/null +++ b/docs/src/DataFixtures/AppFixtures.php @@ -0,0 +1,17 @@ +persist($product); + + $manager->flush(); + } +} diff --git a/docs/src/Entity/.gitignore b/docs/src/Entity/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/src/Kernel.php b/docs/src/Kernel.php index 822949d5da7..3ef1fb95d82 100644 --- a/docs/src/Kernel.php +++ b/docs/src/Kernel.php @@ -1,24 +1,159 @@ getConfigDir(); + + $container->import($configDir.'/{packages}/*.{php,yaml}'); + $container->import($configDir.'/{packages}/'.$this->environment.'/*.{php,yaml}'); + + $services = $container->services() + ->defaults() + ->autowire() + ->autoconfigure() + ; + + $classes = get_declared_classes(); + $resources = []; + + foreach ($classes as $class) { + $refl = new ReflectionClass($class); + $ns = $refl->getNamespaceName(); + if (0 !== strpos($ns, 'PDG') && 0 !== strpos($ns, 'App')) { + continue; + } + + $services->set($class); + + if ($refl->getAttributes(ApiResource::class, \ReflectionAttribute::IS_INSTANCEOF)) { + $resources[] = $class; + } + } + + $services->set(StaticResourceNameCollectionFactory::class)->args(['$classes' => $resources]); + + $container->parameters()->set( + 'database_url', + sprintf('sqlite:///%s/%s', $this->getDBDir(), 'data.db') + ); + + if (function_exists('App\DependencyInjection\configure')) { + configure($container); + } + } + + public function request(?Request $request = null): void + { + if (null === $request && function_exists('App\Playground\request')) { + $request = request(); + } + + $request = $request ?? Request::create('/docs.json'); + $response = $this->handle($request); + + $this->terminate($request, $response); + } + + public function getDBDir(): string + { + return $this->getProjectDir() . '/var/databases/' . $this->guide; + } + + public function executeMigrations(string $direction = Direction::UP): void + { + $migrationClasses = $this->getDeclaredClassesForNamespace('DoctrineMigrations'); + + if (!$migrationClasses) { + return; + } + $this->boot(); + @mkdir('var/databases/' . $this->guide, recursive: true); + + foreach ($migrationClasses as $migrationClass) { + if ("Doctrine\Migrations\AbstractMigration" !== (new ReflectionClass($migrationClass))->getParentClass()->getName()) { + continue; + } + $conf = new Configuration(); + $conf->addMigrationClass($migrationClass); + $conf->setTransactional(true); + $conf->setCheckDatabasePlatform(true); + $meta = new TableMetadataStorageConfiguration(); + $meta->setTableName('doctrine_migration_versions'); + $conf->setMetadataStorageConfiguration($meta); + + $confLoader = new ExistingConfiguration($conf); + /** @var EntityManagerInterface $em */ + $em = $this->getContainer()->get('doctrine.orm.entity_manager'); + $loader = new ExistingEntityManager($em); + $dependencyFactory = DependencyFactory::fromEntityManager($confLoader, $loader); + + $dependencyFactory->getMetadataStorage()->ensureInitialized(); + $executed = $dependencyFactory->getMetadataStorage()->getExecutedMigrations(); + + if ($executed->hasMigration(new Version($migrationClass)) && Direction::DOWN !== $direction) { + continue; + } + + $planCalculator = $dependencyFactory->getMigrationPlanCalculator(); + $plan = $planCalculator->getPlanForVersions([new Version($migrationClass)], $direction); + $migrator = $dependencyFactory->getMigrator(); + $migratorConfigurationFactory = $dependencyFactory->getConsoleInputMigratorConfigurationFactory(); + $migratorConfiguration = $migratorConfigurationFactory->getMigratorConfiguration(new ArrayInput([])); + + $migrator->migrate($plan, $migratorConfiguration); + } + } + + public function getDeclaredClassesForNamespace(string $namespace): array + { + return array_filter(get_declared_classes(), static function (string $class) use ($namespace): bool { + return str_starts_with($class, $namespace); + }); + } + + public function getCacheDir(): string + { + if($this->guide === null){ + return parent::getCacheDir(); + } + + return parent::getCacheDir().'/'.$this->guide; } - public function registerBundles(): iterable + public function getProjectDir(): string { - return [ - new FrameworkBundle(), - ]; + return \dirname(__DIR__); } } + diff --git a/docs/src/Metadata/Resource/Factory/StaticResourceNameCollectionFactory.php b/docs/src/Metadata/Resource/Factory/StaticResourceNameCollectionFactory.php new file mode 100644 index 00000000000..15c714fba39 --- /dev/null +++ b/docs/src/Metadata/Resource/Factory/StaticResourceNameCollectionFactory.php @@ -0,0 +1,35 @@ +classes; + if ($this->decorated) { + foreach ($this->decorated->create() as $resourceClass) { + $classes[] = $resourceClass; + } + } + + return new ResourceNameCollection($this->classes); + } +} diff --git a/docs/src/Repository/.gitignore b/docs/src/Repository/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/symfony.lock b/docs/symfony.lock new file mode 100644 index 00000000000..ff6c91c6f4f --- /dev/null +++ b/docs/symfony.lock @@ -0,0 +1,193 @@ +{ + "dama/doctrine-test-bundle": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "4.0", + "ref": "56eaa387b5e48ebcc7c95a893b47dfa1ad51449c" + } + }, + "doctrine/annotations": { + "version": "2.0", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.10", + "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05" + } + }, + "doctrine/doctrine-bundle": { + "version": "2.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.8", + "ref": "c38df294eea859b4e2d5f252d4e973f55a264f84" + }, + "files": [ + "config/packages/doctrine.yaml", + "src/Entity/.gitignore", + "src/Repository/.gitignore" + ] + }, + "doctrine/doctrine-fixtures-bundle": { + "version": "3.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "1f5514cfa15b947298df4d771e694e578d4c204d" + }, + "files": [ + "src/DataFixtures/AppFixtures.php" + ] + }, + "doctrine/doctrine-migrations-bundle": { + "version": "3.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.1", + "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33" + }, + "files": [ + "config/packages/doctrine_migrations.yaml", + "migrations/.gitignore" + ] + }, + "nelmio/cors-bundle": { + "version": "2.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.5", + "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" + }, + "files": [ + "config/packages/nelmio_cors.yaml" + ] + }, + "phpunit/phpunit": { + "version": "9.5", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "9.3", + "ref": "a6249a6c4392e9169b87abf93225f7f9f59025e6" + }, + "files": [ + ".env.test", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, + "symfony/console": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" + }, + "files": [ + "bin/console" + ] + }, + "symfony/flex": { + "version": "2.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172" + }, + "files": [ + ".env" + ] + }, + "symfony/framework-bundle": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.2", + "ref": "af47254c5e4cd543e6af3e4508298ffebbdaddd3" + }, + "files": [ + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/preload.php", + "config/routes/framework.yaml", + "config/services.yaml", + "public/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/phpunit-bridge": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "819d3d2ffa4590eba0b8f4f3e5e89415ee4e45c3" + }, + "files": [ + ".env.test", + "bin/phpunit", + "phpunit.xml.dist", + "tests/bootstrap.php" + ] + }, + "symfony/routing": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.2", + "ref": "e0a11b4ccb8c9e70b574ff5ad3dfdcd41dec5aa6" + }, + "files": [ + "config/packages/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/security-bundle": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "8a5b112826f7d3d5b07027f93786ae11a1c7de48" + }, + "files": [ + "config/packages/security.yaml" + ] + }, + "symfony/twig-bundle": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.4", + "ref": "bb2178c57eee79e6be0b297aa96fc0c0def81387" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/validator": { + "version": "6.2", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c" + }, + "files": [ + "config/packages/validator.yaml" + ] + } +} diff --git a/docs/templates/base.html.twig b/docs/templates/base.html.twig new file mode 100644 index 00000000000..d4f83f7f8bf --- /dev/null +++ b/docs/templates/base.html.twig @@ -0,0 +1,19 @@ + + + + + {% block title %}Welcome!{% endblock %} + + {# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #} + {% block stylesheets %} + {{ encore_entry_link_tags('app') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app') }} + {% endblock %} + + + {% block body %}{% endblock %} + + diff --git a/docs/tests/bootstrap.php b/docs/tests/bootstrap.php new file mode 100644 index 00000000000..469dccee498 --- /dev/null +++ b/docs/tests/bootstrap.php @@ -0,0 +1,11 @@ +bootEnv(dirname(__DIR__).'/.env'); +}