├── .env ├── .env.test ├── .gitignore ├── README.md ├── bin ├── console └── phpunit ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── packages │ ├── api_platform.yaml │ ├── cache.yaml │ ├── dev │ │ ├── debug.yaml │ │ ├── monolog.yaml │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── messenger.yaml │ ├── nelmio_cors.yaml │ ├── prod │ │ ├── deprecations.yaml │ │ ├── doctrine.yaml │ │ └── monolog.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── test │ │ ├── doctrine.yaml │ │ ├── monolog.yaml │ │ ├── validator.yaml │ │ └── web_profiler.yaml │ ├── twig.yaml │ └── validator.yaml ├── preload.php ├── routes.yaml ├── routes │ ├── annotations.yaml │ ├── api_platform.yaml │ ├── dev │ │ └── web_profiler.yaml │ └── framework.yaml └── services.yaml ├── migrations └── Version20211031095709.php ├── phpunit.xml.dist ├── public └── index.php ├── src ├── Application │ └── Panda │ │ ├── Command │ │ └── FeedPanda.php │ │ ├── Exception │ │ └── PandaNotFoundException.php │ │ ├── Handler │ │ └── FeedPandaHandler.php │ │ └── Query │ │ └── GetPandaOutput.php ├── Domain │ └── Panda │ │ ├── Model │ │ ├── Bamboo.php │ │ ├── Panda.php │ │ └── PandaId.php │ │ └── PandaRepository.php └── Infrastructure │ ├── Kernel.php │ └── Panda │ ├── ApiPlatform │ ├── IdentifierDenormalizer │ │ └── PandaIdentifierDenormalizer.php │ └── Transformer │ │ └── GetPandaOutputTransformer.php │ └── Repository │ ├── PandaRepositoryUsingDoctrine.php │ └── PandaRepositoryUsingMemory.php ├── symfony.lock ├── templates └── base.html.twig └── tests ├── Acceptance └── FeedPandaServiceTest.php └── bootstrap.php /.env: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | APP_ENV=dev 3 | APP_SECRET=72627d1e2f8a2799c84ba87c19b7b260 4 | ###< symfony/framework-bundle ### 5 | 6 | ###> doctrine/doctrine-bundle ### 7 | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 8 | # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml 9 | # 10 | DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" 11 | # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" 12 | # DATABASE_URL="postgresql://symfony:ChangeMe@127.0.0.1:5432/app?serverVersion=13&charset=utf8" 13 | ###< doctrine/doctrine-bundle ### 14 | 15 | ###> nelmio/cors-bundle ### 16 | CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' 17 | ###< nelmio/cors-bundle ### 18 | 19 | ###> symfony/messenger ### 20 | # Choose one of the transports below 21 | # MESSENGER_TRANSPORT_DSN=doctrine://default 22 | # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages 23 | # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages 24 | ###< symfony/messenger ### 25 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ###> symfony/framework-bundle ### 3 | /.env.local 4 | /.env.local.php 5 | /.env.*.local 6 | /config/secrets/prod/prod.decrypt.private.php 7 | /public/bundles/ 8 | /var/ 9 | /vendor/ 10 | ###< symfony/framework-bundle ### 11 | 12 | ###> symfony/phpunit-bridge ### 13 | .phpunit.result.cache 14 | /phpunit.xml 15 | ###< symfony/phpunit-bridge ### 16 | 17 | ###> phpunit/phpunit ### 18 | /phpunit.xml 19 | .phpunit.result.cache 20 | ###< phpunit/phpunit ### 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | apip-ddd 2 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =7.2.5", 8 | "ext-ctype": "*", 9 | "ext-iconv": "*", 10 | "api-platform/core": "^2.6", 11 | "composer/package-versions-deprecated": "1.11.99.4", 12 | "doctrine/annotations": "^1.0", 13 | "doctrine/doctrine-bundle": "^2.4", 14 | "doctrine/doctrine-migrations-bundle": "^3.2", 15 | "doctrine/orm": "^2.10", 16 | "nelmio/cors-bundle": "^2.1", 17 | "phpdocumentor/reflection-docblock": "^5.3", 18 | "symfony/asset": "5.3.*", 19 | "symfony/console": "5.3.*", 20 | "symfony/dotenv": "5.3.*", 21 | "symfony/expression-language": "5.3.*", 22 | "symfony/flex": "^1.3.1", 23 | "symfony/framework-bundle": "5.3.*", 24 | "symfony/messenger": "5.3.*", 25 | "symfony/property-access": "5.3.*", 26 | "symfony/property-info": "5.3.*", 27 | "symfony/proxy-manager-bridge": "5.3.*", 28 | "symfony/runtime": "5.3.*", 29 | "symfony/security-bundle": "5.3.*", 30 | "symfony/serializer": "5.3.*", 31 | "symfony/twig-bundle": "5.3.*", 32 | "symfony/validator": "5.3.*", 33 | "symfony/yaml": "5.3.*" 34 | }, 35 | "require-dev": { 36 | "phpunit/phpunit": "^9.5", 37 | "symfony/browser-kit": "5.3.*", 38 | "symfony/css-selector": "5.3.*", 39 | "symfony/debug-bundle": "5.3.*", 40 | "symfony/monolog-bundle": "^3.0", 41 | "symfony/phpunit-bridge": "^5.3", 42 | "symfony/stopwatch": "5.3.*", 43 | "symfony/web-profiler-bundle": "5.3.*" 44 | }, 45 | "config": { 46 | "optimize-autoloader": true, 47 | "preferred-install": { 48 | "*": "dist" 49 | }, 50 | "sort-packages": true 51 | }, 52 | "autoload": { 53 | "psr-4": { 54 | "Domain\\": "src/Domain", 55 | "Application\\": "src/Application", 56 | "Infrastructure\\": "src/Infrastructure" 57 | } 58 | }, 59 | "autoload-dev": { 60 | "psr-4": { 61 | "App\\Tests\\": "tests/" 62 | } 63 | }, 64 | "replace": { 65 | "symfony/polyfill-ctype": "*", 66 | "symfony/polyfill-iconv": "*", 67 | "symfony/polyfill-php72": "*" 68 | }, 69 | "scripts": { 70 | "auto-scripts": { 71 | "cache:clear": "symfony-cmd", 72 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 73 | }, 74 | "post-install-cmd": [ 75 | "@auto-scripts" 76 | ], 77 | "post-update-cmd": [ 78 | "@auto-scripts" 79 | ] 80 | }, 81 | "conflict": { 82 | "symfony/symfony": "*" 83 | }, 84 | "extra": { 85 | "symfony": { 86 | "allow-contrib": false, 87 | "require": "5.3.*" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 6 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 7 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 8 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 9 | Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], 10 | ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], 11 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 12 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 13 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], 14 | ]; 15 | -------------------------------------------------------------------------------- /config/packages/api_platform.yaml: -------------------------------------------------------------------------------- 1 | api_platform: 2 | mapping: 3 | paths: 4 | - '%kernel.project_dir%/src/Application/Panda/Command' 5 | - '%kernel.project_dir%/src/Domain/Panda/Model' 6 | patch_formats: 7 | json: ['application/merge-patch+json'] 8 | swagger: 9 | versions: [3] 10 | exception_to_status: 11 | Application\Panda\Exception\PandaNotFoundException: 404 12 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /config/packages/dev/debug.yaml: -------------------------------------------------------------------------------- 1 | debug: 2 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. 3 | # See the "server:dump" command to start a new server. 4 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" 5 | -------------------------------------------------------------------------------- /config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: debug 7 | channels: ["!event"] 8 | # uncomment to get logging in your browser 9 | # you may have to allow bigger header sizes in your Web server configuration 10 | #firephp: 11 | # type: firephp 12 | # level: info 13 | #chromephp: 14 | # type: chromephp 15 | # level: info 16 | console: 17 | type: console 18 | process_psr_3_messages: false 19 | channels: ["!event", "!doctrine", "!console"] 20 | -------------------------------------------------------------------------------- /config/packages/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { only_exceptions: false } 7 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(resolve:DATABASE_URL)%' 4 | orm: 5 | auto_generate_proxy_classes: true 6 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 7 | auto_mapping: true 8 | mappings: 9 | Panda_Model: 10 | is_bundle: false 11 | type: attribute 12 | dir: '%kernel.project_dir%/src/Domain/Panda/Model' 13 | prefix: 'Domain\Panda\Model' 14 | alias: Panda 15 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | migrations_paths: 3 | 'DoctrineMigrations': '%kernel.project_dir%/migrations' 4 | enable_profiler: '%kernel.debug%' 5 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | http_method_override: false 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: null 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | storage_factory_id: session.storage.factory.native 14 | 15 | #esi: true 16 | #fragments: true 17 | php_errors: 18 | log: true 19 | 20 | when@test: 21 | framework: 22 | test: true 23 | session: 24 | storage_factory_id: session.storage.factory.mock_file 25 | -------------------------------------------------------------------------------- /config/packages/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | transports: 4 | sync: 'sync://' 5 | routing: 6 | 'Application\Panda\Command\FeedPanda': sync 7 | -------------------------------------------------------------------------------- /config/packages/nelmio_cors.yaml: -------------------------------------------------------------------------------- 1 | nelmio_cors: 2 | defaults: 3 | origin_regex: true 4 | allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] 5 | allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] 6 | allow_headers: ['Content-Type', 'Authorization'] 7 | expose_headers: ['Link'] 8 | max_age: 3600 9 | paths: 10 | '^/': null 11 | -------------------------------------------------------------------------------- /config/packages/prod/deprecations.yaml: -------------------------------------------------------------------------------- 1 | # As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists 2 | #monolog: 3 | # channels: [deprecation] 4 | # handlers: 5 | # deprecation: 6 | # type: stream 7 | # channels: [deprecation] 8 | # path: php://stderr 9 | -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | auto_generate_proxy_classes: false 4 | query_cache_driver: 5 | type: pool 6 | pool: doctrine.system_cache_pool 7 | result_cache_driver: 8 | type: pool 9 | pool: doctrine.result_cache_pool 10 | 11 | framework: 12 | cache: 13 | pools: 14 | doctrine.result_cache_pool: 15 | adapter: cache.app 16 | doctrine.system_cache_pool: 17 | adapter: cache.system 18 | -------------------------------------------------------------------------------- /config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks 9 | nested: 10 | type: stream 11 | path: php://stderr 12 | level: debug 13 | formatter: monolog.formatter.json 14 | console: 15 | type: console 16 | process_psr_3_messages: false 17 | channels: ["!event", "!doctrine"] 18 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | 5 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 6 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 7 | #default_uri: http://localhost 8 | 9 | when@prod: 10 | framework: 11 | router: 12 | strict_requirements: null 13 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | enable_authenticator_manager: true 3 | # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords 4 | password_hashers: 5 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' 6 | # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider 7 | providers: 8 | users_in_memory: { memory: null } 9 | firewalls: 10 | dev: 11 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 12 | security: false 13 | main: 14 | lazy: true 15 | provider: users_in_memory 16 | 17 | # activate different ways to authenticate 18 | # https://symfony.com/doc/current/security.html#the-firewall 19 | 20 | # https://symfony.com/doc/current/security/impersonating_user.html 21 | # switch_user: true 22 | 23 | # Easy way to control access for large sections of your site 24 | # Note: Only the *first* access control that matches will be used 25 | access_control: 26 | # - { path: ^/admin, roles: ROLE_ADMIN } 27 | # - { path: ^/profile, roles: ROLE_USER } 28 | -------------------------------------------------------------------------------- /config/packages/test/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | # "TEST_TOKEN" is typically set by ParaTest 4 | dbname_suffix: '_test%env(default::TEST_TOKEN)%' 5 | -------------------------------------------------------------------------------- /config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | channels: ["!event"] 9 | nested: 10 | type: stream 11 | path: "%kernel.logs_dir%/%kernel.environment%.log" 12 | level: debug 13 | -------------------------------------------------------------------------------- /config/packages/test/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | not_compromised_password: false 4 | -------------------------------------------------------------------------------- /config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | 4 | when@test: 5 | twig: 6 | strict_variables: true 7 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | 5 | # Enables validator auto-mapping support. 6 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 7 | #auto_mapping: 8 | # App\Entity\: [] 9 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE panda (id INTEGER NOT NULL, hunger_amount INTEGER NOT NULL, PRIMARY KEY(id))'); 19 | } 20 | 21 | public function down(Schema $schema): void 22 | { 23 | // this down() migration is auto-generated, please modify it to your needs 24 | $this->addSql('DROP TABLE panda'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | tests 23 | 24 | 25 | 26 | 27 | 28 | src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | ['path' => '/feed-panda', 'messenger' => true, 'output' => false, 'status' => 204], 11 | ], 12 | itemOperations: [], 13 | )] 14 | class FeedPanda 15 | { 16 | public function __construct( 17 | #[ApiProperty(identifier: true)] 18 | public int $pandaId, 19 | public int $bambooLength 20 | ) { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Application/Panda/Exception/PandaNotFoundException.php: -------------------------------------------------------------------------------- 1 | id = $id; 15 | } 16 | 17 | public static function fromId(PandaId $id, ?string $message = null): self 18 | { 19 | return new self($id->getId(), $message ?? "Panda {$id->getId()} not found."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Application/Panda/Handler/FeedPandaHandler.php: -------------------------------------------------------------------------------- 1 | pandaId); 22 | $panda = $this->pandaRepository->get($id); 23 | if (!$panda) { 24 | throw PandaNotFoundException::fromId($id); 25 | } 26 | $panda->feed(new Bamboo($command->bambooLength)); 27 | $this->pandaRepository->save($panda); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Application/Panda/Query/GetPandaOutput.php: -------------------------------------------------------------------------------- 1 | getId()->getId(), 19 | hungry: $panda->isHungry(), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Domain/Panda/Model/Bamboo.php: -------------------------------------------------------------------------------- 1 | length; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Domain/Panda/Model/Panda.php: -------------------------------------------------------------------------------- 1 | ['path' => '/pandas/{id}', 'output' => GetPandaOutput::class], 17 | ], 18 | )] 19 | class Panda 20 | { 21 | #[Id] 22 | #[Column(type: 'integer')] 23 | #[ApiProperty(identifier: true)] 24 | private int $id; 25 | 26 | #[Column(type: 'integer')] 27 | private int $hungerAmount; 28 | 29 | public function __construct(PandaId $id) 30 | { 31 | $this->id = $id->getId(); 32 | $this->hungerAmount = 0; 33 | } 34 | 35 | public function getId(): PandaId 36 | { 37 | return new PandaId($this->id); 38 | } 39 | 40 | public function doExercise(): void 41 | { 42 | $this->hungerAmount += 5; 43 | } 44 | 45 | public function feed(Bamboo $bamboo): void 46 | { 47 | $this->hungerAmount = max(0, $this->hungerAmount - $bamboo->getLength()); 48 | } 49 | 50 | public function isHungry(): bool 51 | { 52 | return $this->hungerAmount > 0; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Domain/Panda/Model/PandaId.php: -------------------------------------------------------------------------------- 1 | id; 15 | } 16 | 17 | public function __toString(): string 18 | { 19 | return (string) $this->id; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Domain/Panda/PandaRepository.php: -------------------------------------------------------------------------------- 1 | import('../../config/{packages}/*.yaml'); 17 | $container->import('../../config/{packages}/'.$this->environment.'/*.yaml'); 18 | 19 | $container->import('../../config/services.yaml'); 20 | $container->import('../../config/{services}_'.$this->environment.'.yaml'); 21 | } 22 | 23 | protected function configureRoutes(RoutingConfigurator $routes): void 24 | { 25 | $routes->import('../../config/{routes}/'.$this->environment.'/*.yaml'); 26 | $routes->import('../../config/{routes}/*.yaml'); 27 | 28 | $routes->import('../../config/routes.yaml'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Infrastructure/Panda/ApiPlatform/IdentifierDenormalizer/PandaIdentifierDenormalizer.php: -------------------------------------------------------------------------------- 1 | getId(); 17 | } 18 | 19 | public function supportsDenormalization($data, $type, $format = null): bool 20 | { 21 | return $type === PandaId::class; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Infrastructure/Panda/ApiPlatform/Transformer/GetPandaOutputTransformer.php: -------------------------------------------------------------------------------- 1 | find($id->getId()); 21 | } 22 | 23 | public function save(Panda $panda): void 24 | { 25 | $this->_em->persist($panda); 26 | $this->_em->flush(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Infrastructure/Panda/Repository/PandaRepositoryUsingMemory.php: -------------------------------------------------------------------------------- 1 | store[$id->getId()] ?? null; 17 | } 18 | 19 | public function save(Panda $panda): void 20 | { 21 | $this->store[$panda->getId()->getId()] = $panda; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "api-platform/core": { 3 | "version": "2.6", 4 | "recipe": { 5 | "repo": "github.com/symfony/recipes", 6 | "branch": "master", 7 | "version": "2.5", 8 | "ref": "05b57782a78c21a664a42055dc11cf1954ca36bb" 9 | }, 10 | "files": [ 11 | "config/routes/api_platform.yaml", 12 | "config/packages/api_platform.yaml", 13 | "src/Entity/.gitignore" 14 | ] 15 | }, 16 | "composer/package-versions-deprecated": { 17 | "version": "1.11.99.4" 18 | }, 19 | "doctrine/annotations": { 20 | "version": "1.13", 21 | "recipe": { 22 | "repo": "github.com/symfony/recipes", 23 | "branch": "master", 24 | "version": "1.0", 25 | "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" 26 | }, 27 | "files": [ 28 | "config/routes/annotations.yaml" 29 | ] 30 | }, 31 | "doctrine/cache": { 32 | "version": "2.1.1" 33 | }, 34 | "doctrine/collections": { 35 | "version": "1.6.8" 36 | }, 37 | "doctrine/common": { 38 | "version": "3.2.0" 39 | }, 40 | "doctrine/dbal": { 41 | "version": "3.1.3" 42 | }, 43 | "doctrine/deprecations": { 44 | "version": "v0.5.3" 45 | }, 46 | "doctrine/doctrine-bundle": { 47 | "version": "2.4", 48 | "recipe": { 49 | "repo": "github.com/symfony/recipes", 50 | "branch": "master", 51 | "version": "2.4", 52 | "ref": "032f52ed50a27762b78ca6a2aaf432958c473553" 53 | }, 54 | "files": [ 55 | "config/packages/doctrine.yaml", 56 | "config/packages/test/doctrine.yaml", 57 | "config/packages/prod/doctrine.yaml", 58 | "src/Repository/.gitignore", 59 | "src/Entity/.gitignore" 60 | ] 61 | }, 62 | "doctrine/doctrine-migrations-bundle": { 63 | "version": "3.2", 64 | "recipe": { 65 | "repo": "github.com/symfony/recipes", 66 | "branch": "master", 67 | "version": "3.1", 68 | "ref": "ee609429c9ee23e22d6fa5728211768f51ed2818" 69 | }, 70 | "files": [ 71 | "config/packages/doctrine_migrations.yaml", 72 | "migrations/.gitignore" 73 | ] 74 | }, 75 | "doctrine/event-manager": { 76 | "version": "1.1.1" 77 | }, 78 | "doctrine/inflector": { 79 | "version": "2.0.4" 80 | }, 81 | "doctrine/instantiator": { 82 | "version": "1.4.0" 83 | }, 84 | "doctrine/lexer": { 85 | "version": "1.2.1" 86 | }, 87 | "doctrine/migrations": { 88 | "version": "3.3.0" 89 | }, 90 | "doctrine/orm": { 91 | "version": "2.10.2" 92 | }, 93 | "doctrine/persistence": { 94 | "version": "2.2.3" 95 | }, 96 | "doctrine/sql-formatter": { 97 | "version": "1.1.1" 98 | }, 99 | "fig/link-util": { 100 | "version": "1.2.0" 101 | }, 102 | "friendsofphp/proxy-manager-lts": { 103 | "version": "v1.0.5" 104 | }, 105 | "laminas/laminas-code": { 106 | "version": "4.4.3" 107 | }, 108 | "monolog/monolog": { 109 | "version": "2.3.5" 110 | }, 111 | "myclabs/deep-copy": { 112 | "version": "1.10.2" 113 | }, 114 | "nelmio/cors-bundle": { 115 | "version": "2.1", 116 | "recipe": { 117 | "repo": "github.com/symfony/recipes", 118 | "branch": "master", 119 | "version": "1.5", 120 | "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c" 121 | }, 122 | "files": [ 123 | "config/packages/nelmio_cors.yaml" 124 | ] 125 | }, 126 | "nikic/php-parser": { 127 | "version": "v4.13.0" 128 | }, 129 | "phar-io/manifest": { 130 | "version": "2.0.3" 131 | }, 132 | "phar-io/version": { 133 | "version": "3.1.0" 134 | }, 135 | "phpdocumentor/reflection-common": { 136 | "version": "2.2.0" 137 | }, 138 | "phpdocumentor/reflection-docblock": { 139 | "version": "5.3.0" 140 | }, 141 | "phpdocumentor/type-resolver": { 142 | "version": "1.5.1" 143 | }, 144 | "phpspec/prophecy": { 145 | "version": "1.14.0" 146 | }, 147 | "phpunit/php-code-coverage": { 148 | "version": "9.2.8" 149 | }, 150 | "phpunit/php-file-iterator": { 151 | "version": "3.0.5" 152 | }, 153 | "phpunit/php-invoker": { 154 | "version": "3.1.1" 155 | }, 156 | "phpunit/php-text-template": { 157 | "version": "2.0.4" 158 | }, 159 | "phpunit/php-timer": { 160 | "version": "5.0.3" 161 | }, 162 | "phpunit/phpunit": { 163 | "version": "9.5", 164 | "recipe": { 165 | "repo": "github.com/symfony/recipes", 166 | "branch": "master", 167 | "version": "9.3", 168 | "ref": "a6249a6c4392e9169b87abf93225f7f9f59025e6" 169 | }, 170 | "files": [ 171 | ".env.test", 172 | "phpunit.xml.dist", 173 | "tests/bootstrap.php" 174 | ] 175 | }, 176 | "psr/cache": { 177 | "version": "2.0.0" 178 | }, 179 | "psr/container": { 180 | "version": "1.1.1" 181 | }, 182 | "psr/event-dispatcher": { 183 | "version": "1.0.0" 184 | }, 185 | "psr/link": { 186 | "version": "1.1.1" 187 | }, 188 | "psr/log": { 189 | "version": "2.0.0" 190 | }, 191 | "sebastian/cli-parser": { 192 | "version": "1.0.1" 193 | }, 194 | "sebastian/code-unit": { 195 | "version": "1.0.8" 196 | }, 197 | "sebastian/code-unit-reverse-lookup": { 198 | "version": "2.0.3" 199 | }, 200 | "sebastian/comparator": { 201 | "version": "4.0.6" 202 | }, 203 | "sebastian/complexity": { 204 | "version": "2.0.2" 205 | }, 206 | "sebastian/diff": { 207 | "version": "4.0.4" 208 | }, 209 | "sebastian/environment": { 210 | "version": "5.1.3" 211 | }, 212 | "sebastian/exporter": { 213 | "version": "4.0.3" 214 | }, 215 | "sebastian/global-state": { 216 | "version": "5.0.3" 217 | }, 218 | "sebastian/lines-of-code": { 219 | "version": "1.0.3" 220 | }, 221 | "sebastian/object-enumerator": { 222 | "version": "4.0.4" 223 | }, 224 | "sebastian/object-reflector": { 225 | "version": "2.0.4" 226 | }, 227 | "sebastian/recursion-context": { 228 | "version": "4.0.4" 229 | }, 230 | "sebastian/resource-operations": { 231 | "version": "3.0.3" 232 | }, 233 | "sebastian/type": { 234 | "version": "2.3.4" 235 | }, 236 | "sebastian/version": { 237 | "version": "3.0.2" 238 | }, 239 | "symfony/amqp-messenger": { 240 | "version": "v5.3.7" 241 | }, 242 | "symfony/asset": { 243 | "version": "v5.3.4" 244 | }, 245 | "symfony/browser-kit": { 246 | "version": "v5.3.4" 247 | }, 248 | "symfony/cache": { 249 | "version": "v5.3.10" 250 | }, 251 | "symfony/cache-contracts": { 252 | "version": "v2.4.0" 253 | }, 254 | "symfony/config": { 255 | "version": "v5.3.10" 256 | }, 257 | "symfony/console": { 258 | "version": "5.3", 259 | "recipe": { 260 | "repo": "github.com/symfony/recipes", 261 | "branch": "master", 262 | "version": "5.3", 263 | "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047" 264 | }, 265 | "files": [ 266 | "bin/console" 267 | ] 268 | }, 269 | "symfony/css-selector": { 270 | "version": "v5.3.4" 271 | }, 272 | "symfony/debug-bundle": { 273 | "version": "5.3", 274 | "recipe": { 275 | "repo": "github.com/symfony/recipes", 276 | "branch": "master", 277 | "version": "4.1", 278 | "ref": "0ce7a032d344fb7b661cd25d31914cd703ad445b" 279 | }, 280 | "files": [ 281 | "config/packages/dev/debug.yaml" 282 | ] 283 | }, 284 | "symfony/dependency-injection": { 285 | "version": "v5.3.10" 286 | }, 287 | "symfony/deprecation-contracts": { 288 | "version": "v2.4.0" 289 | }, 290 | "symfony/doctrine-bridge": { 291 | "version": "v5.3.8" 292 | }, 293 | "symfony/doctrine-messenger": { 294 | "version": "v5.3.10" 295 | }, 296 | "symfony/dom-crawler": { 297 | "version": "v5.3.7" 298 | }, 299 | "symfony/dotenv": { 300 | "version": "v5.3.10" 301 | }, 302 | "symfony/error-handler": { 303 | "version": "v5.3.7" 304 | }, 305 | "symfony/event-dispatcher": { 306 | "version": "v5.3.7" 307 | }, 308 | "symfony/event-dispatcher-contracts": { 309 | "version": "v2.4.0" 310 | }, 311 | "symfony/expression-language": { 312 | "version": "v5.3.7" 313 | }, 314 | "symfony/filesystem": { 315 | "version": "v5.3.4" 316 | }, 317 | "symfony/finder": { 318 | "version": "v5.3.7" 319 | }, 320 | "symfony/flex": { 321 | "version": "1.17", 322 | "recipe": { 323 | "repo": "github.com/symfony/recipes", 324 | "branch": "master", 325 | "version": "1.0", 326 | "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" 327 | }, 328 | "files": [ 329 | ".env" 330 | ] 331 | }, 332 | "symfony/framework-bundle": { 333 | "version": "5.3", 334 | "recipe": { 335 | "repo": "github.com/symfony/recipes", 336 | "branch": "master", 337 | "version": "5.3", 338 | "ref": "414ba00ad43fa71be42c7906a551f1831716b03c" 339 | }, 340 | "files": [ 341 | "config/services.yaml", 342 | "config/routes/framework.yaml", 343 | "config/preload.php", 344 | "config/packages/cache.yaml", 345 | "config/packages/framework.yaml", 346 | "public/index.php", 347 | "src/Kernel.php", 348 | "src/Controller/.gitignore" 349 | ] 350 | }, 351 | "symfony/http-client-contracts": { 352 | "version": "v2.4.0" 353 | }, 354 | "symfony/http-foundation": { 355 | "version": "v5.3.10" 356 | }, 357 | "symfony/http-kernel": { 358 | "version": "v5.3.10" 359 | }, 360 | "symfony/messenger": { 361 | "version": "5.3", 362 | "recipe": { 363 | "repo": "github.com/symfony/recipes", 364 | "branch": "master", 365 | "version": "4.3", 366 | "ref": "25e3c964d3aee480b3acc3114ffb7940c89edfed" 367 | }, 368 | "files": [ 369 | "config/packages/messenger.yaml" 370 | ] 371 | }, 372 | "symfony/monolog-bridge": { 373 | "version": "v5.3.7" 374 | }, 375 | "symfony/monolog-bundle": { 376 | "version": "3.7", 377 | "recipe": { 378 | "repo": "github.com/symfony/recipes", 379 | "branch": "master", 380 | "version": "3.7", 381 | "ref": "a7bace7dbc5a7ed5608dbe2165e0774c87175fe6" 382 | }, 383 | "files": [ 384 | "config/packages/test/monolog.yaml", 385 | "config/packages/prod/monolog.yaml", 386 | "config/packages/prod/deprecations.yaml", 387 | "config/packages/dev/monolog.yaml" 388 | ] 389 | }, 390 | "symfony/password-hasher": { 391 | "version": "v5.3.8" 392 | }, 393 | "symfony/phpunit-bridge": { 394 | "version": "5.3", 395 | "recipe": { 396 | "repo": "github.com/symfony/recipes", 397 | "branch": "master", 398 | "version": "5.3", 399 | "ref": "97cb3dc7b0f39c7cfc4b7553504c9d7b7795de96" 400 | }, 401 | "files": [ 402 | ".env.test", 403 | "bin/phpunit", 404 | "phpunit.xml.dist", 405 | "tests/bootstrap.php" 406 | ] 407 | }, 408 | "symfony/polyfill-intl-grapheme": { 409 | "version": "v1.23.1" 410 | }, 411 | "symfony/polyfill-intl-normalizer": { 412 | "version": "v1.23.0" 413 | }, 414 | "symfony/polyfill-mbstring": { 415 | "version": "v1.23.1" 416 | }, 417 | "symfony/polyfill-php73": { 418 | "version": "v1.23.0" 419 | }, 420 | "symfony/polyfill-php80": { 421 | "version": "v1.23.1" 422 | }, 423 | "symfony/polyfill-php81": { 424 | "version": "v1.23.0" 425 | }, 426 | "symfony/property-access": { 427 | "version": "v5.3.8" 428 | }, 429 | "symfony/property-info": { 430 | "version": "v5.3.8" 431 | }, 432 | "symfony/proxy-manager-bridge": { 433 | "version": "v5.3.4" 434 | }, 435 | "symfony/redis-messenger": { 436 | "version": "v5.3.10" 437 | }, 438 | "symfony/routing": { 439 | "version": "5.3", 440 | "recipe": { 441 | "repo": "github.com/symfony/recipes", 442 | "branch": "master", 443 | "version": "5.3", 444 | "ref": "44633353926a0382d7dfb0530922c5c0b30fae11" 445 | }, 446 | "files": [ 447 | "config/routes.yaml", 448 | "config/packages/routing.yaml" 449 | ] 450 | }, 451 | "symfony/runtime": { 452 | "version": "v5.3.10" 453 | }, 454 | "symfony/security-bundle": { 455 | "version": "5.3", 456 | "recipe": { 457 | "repo": "github.com/symfony/recipes", 458 | "branch": "master", 459 | "version": "5.3", 460 | "ref": "3307d76caa2d12fb10ade57975beb3d8975df396" 461 | }, 462 | "files": [ 463 | "config/packages/security.yaml" 464 | ] 465 | }, 466 | "symfony/security-core": { 467 | "version": "v5.3.10" 468 | }, 469 | "symfony/security-csrf": { 470 | "version": "v5.3.4" 471 | }, 472 | "symfony/security-guard": { 473 | "version": "v5.3.7" 474 | }, 475 | "symfony/security-http": { 476 | "version": "v5.3.10" 477 | }, 478 | "symfony/serializer": { 479 | "version": "v5.3.10" 480 | }, 481 | "symfony/service-contracts": { 482 | "version": "v2.4.0" 483 | }, 484 | "symfony/stopwatch": { 485 | "version": "v5.3.4" 486 | }, 487 | "symfony/string": { 488 | "version": "v5.3.10" 489 | }, 490 | "symfony/translation-contracts": { 491 | "version": "v2.4.0" 492 | }, 493 | "symfony/twig-bridge": { 494 | "version": "v5.3.7" 495 | }, 496 | "symfony/twig-bundle": { 497 | "version": "5.3", 498 | "recipe": { 499 | "repo": "github.com/symfony/recipes", 500 | "branch": "master", 501 | "version": "5.3", 502 | "ref": "3dd530739a4284e3272274c128dbb7a8140a66f1" 503 | }, 504 | "files": [ 505 | "config/packages/twig.yaml", 506 | "templates/base.html.twig" 507 | ] 508 | }, 509 | "symfony/validator": { 510 | "version": "5.3", 511 | "recipe": { 512 | "repo": "github.com/symfony/recipes", 513 | "branch": "master", 514 | "version": "4.3", 515 | "ref": "3eb8df139ec05414489d55b97603c5f6ca0c44cb" 516 | }, 517 | "files": [ 518 | "config/packages/validator.yaml", 519 | "config/packages/test/validator.yaml" 520 | ] 521 | }, 522 | "symfony/var-dumper": { 523 | "version": "v5.3.10" 524 | }, 525 | "symfony/var-exporter": { 526 | "version": "v5.3.8" 527 | }, 528 | "symfony/web-link": { 529 | "version": "v5.3.4" 530 | }, 531 | "symfony/web-profiler-bundle": { 532 | "version": "5.3", 533 | "recipe": { 534 | "repo": "github.com/symfony/recipes", 535 | "branch": "master", 536 | "version": "3.3", 537 | "ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6" 538 | }, 539 | "files": [ 540 | "config/routes/dev/web_profiler.yaml", 541 | "config/packages/test/web_profiler.yaml", 542 | "config/packages/dev/web_profiler.yaml" 543 | ] 544 | }, 545 | "symfony/yaml": { 546 | "version": "v5.3.6" 547 | }, 548 | "theseer/tokenizer": { 549 | "version": "1.2.1" 550 | }, 551 | "twig/twig": { 552 | "version": "v3.3.3" 553 | }, 554 | "webmozart/assert": { 555 | "version": "1.10.0" 556 | }, 557 | "willdurand/negotiation": { 558 | "version": "3.0.0" 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | {# Run `composer require symfony/webpack-encore-bundle` 7 | and uncomment the following Encore helpers to start using Symfony UX #} 8 | {% block stylesheets %} 9 | {#{{ encore_entry_link_tags('app') }}#} 10 | {% endblock %} 11 | 12 | {% block javascripts %} 13 | {#{{ encore_entry_script_tags('app') }}#} 14 | {% endblock %} 15 | 16 | 17 | {% block body %}{% endblock %} 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/Acceptance/FeedPandaServiceTest.php: -------------------------------------------------------------------------------- 1 | doExercise(); 18 | $repo = new PandaRepositoryUsingMemory(); 19 | $repo->save($panda); 20 | 21 | $service = new FeedPandaHandler($repo); 22 | $service(new FeedPanda(1, 10)); 23 | 24 | $panda = $repo->get(new PandaId(1)); 25 | static::assertFalse($panda->isHungry(), 'Panda should not be hungry after feeding'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | } 12 | --------------------------------------------------------------------------------