├── public ├── img │ ├── 1c.png │ ├── 1d.png │ ├── 5d.png │ ├── 10c.png │ ├── 20d.png │ ├── 25c.png │ ├── Gum.png │ ├── Icon.png │ ├── Soda.png │ └── Chocolate.png └── index.php ├── config ├── packages │ ├── routing.yaml │ ├── dev │ │ └── routing.yaml │ ├── twig.yaml │ ├── doctrine_migrations.yaml │ ├── doctrine.yaml │ ├── prod │ │ └── doctrine.yaml │ └── framework.yaml ├── routes │ ├── annotations.yaml │ └── dev │ │ └── twig.yaml ├── bundles.php └── services.yaml ├── src ├── Domain │ ├── Common │ │ ├── DomainEvent.php │ │ ├── InvalidOperationException.php │ │ ├── ValueObject.php │ │ ├── Utility.php │ │ ├── Handler.php │ │ ├── AggregateRoot.php │ │ └── Entity.php │ ├── Atm │ │ ├── PaymentGateway.php │ │ ├── AtmRepository.php │ │ ├── BalanceChangedEvent.php │ │ ├── AtmDto.php │ │ └── Atm.php │ ├── SnackMachine │ │ ├── SnackRepository.php │ │ ├── SnackMachineRepository.php │ │ ├── SnackMachineDto.php │ │ ├── Slot.php │ │ ├── Snack.php │ │ ├── SnackPile.php │ │ └── SnackMachine.php │ ├── Management │ │ ├── HeadOfficeRepository.php │ │ ├── BalanceChangedEventHandler.php │ │ └── HeadOffice.php │ └── SharedKernel │ │ └── Money.php ├── Infrastructure │ ├── PaymentGatewayStub.php │ ├── Repository │ │ ├── DoctrineSnackRepository.php │ │ ├── DoctrineHeadOfficeRepository.php │ │ ├── DoctrineAtmRepository.php │ │ └── DoctrineSnackMachineRepository.php │ ├── DomainHandlerCompilerPass.php │ ├── Migrations │ │ ├── Version20180502180448.php │ │ ├── Version20180516161021.php │ │ ├── Version20180607093723.php │ │ └── Version20180607235635.php │ └── DomainEventDispatcher.php ├── UI │ ├── HomeController.php │ ├── AtmController.php │ ├── HeadOfficeController.php │ └── SnackMachineController.php └── Kernel.php ├── .gitignore ├── .travis.yml ├── .env ├── phpunit.xml ├── templates ├── base.html.twig ├── home.html.twig ├── atm.html.twig ├── head-office.html.twig └── snack-machine.html.twig ├── tests ├── unit │ ├── SnackPileTest.php │ ├── HeadOfficeTest.php │ ├── AtmTest.php │ ├── MoneyTest.php │ └── SnackMachineTest.php └── integration │ └── IntegrationTest.php ├── LICENSE ├── bin └── console ├── composer.json ├── README.md └── symfony.lock /public/img/1c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabwu/dddinaction/HEAD/public/img/1c.png -------------------------------------------------------------------------------- /public/img/1d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabwu/dddinaction/HEAD/public/img/1d.png -------------------------------------------------------------------------------- /public/img/5d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabwu/dddinaction/HEAD/public/img/5d.png -------------------------------------------------------------------------------- /public/img/10c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabwu/dddinaction/HEAD/public/img/10c.png -------------------------------------------------------------------------------- /public/img/20d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabwu/dddinaction/HEAD/public/img/20d.png -------------------------------------------------------------------------------- /public/img/25c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabwu/dddinaction/HEAD/public/img/25c.png -------------------------------------------------------------------------------- /public/img/Gum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabwu/dddinaction/HEAD/public/img/Gum.png -------------------------------------------------------------------------------- /public/img/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabwu/dddinaction/HEAD/public/img/Icon.png -------------------------------------------------------------------------------- /public/img/Soda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabwu/dddinaction/HEAD/public/img/Soda.png -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: ~ 4 | -------------------------------------------------------------------------------- /config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /public/img/Chocolate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fabwu/dddinaction/HEAD/public/img/Chocolate.png -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: ../../src/UI/ 3 | type: annotation 4 | -------------------------------------------------------------------------------- /config/routes/dev/twig.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@TwigBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /src/Domain/Common/DomainEvent.php: -------------------------------------------------------------------------------- 1 | symfony/framework-bundle ### 4 | /public/bundles/ 5 | /var/ 6 | /vendor/ 7 | ###< symfony/framework-bundle ### 8 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | paths: ['%kernel.project_dir%/templates'] 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' 5 | -------------------------------------------------------------------------------- /src/Domain/Atm/PaymentGateway.php: -------------------------------------------------------------------------------- 1 | isEquals($obj); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Domain/SnackMachine/SnackMachineRepository.php: -------------------------------------------------------------------------------- 1 | delta = $delta; 16 | } 17 | 18 | public function getDelta(): float 19 | { 20 | return $this->delta; 21 | } 22 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # This file is a "template" of which env vars need to be defined for your application 2 | # Copy this file to .env file for development, create environment variables when deploying to production 3 | # https://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration 4 | 5 | ###> symfony/framework-bundle ### 6 | APP_ENV=dev 7 | APP_SECRET=bfc22997577a49fa286b7dc9c1e26990 8 | #TRUSTED_PROXIES=127.0.0.1,127.0.0.2 9 | #TRUSTED_HOSTS=localhost,example.com 10 | ###< symfony/framework-bundle ### 11 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | tests/unit 10 | 11 | 12 | tests/integration 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Domain/Atm/AtmDto.php: -------------------------------------------------------------------------------- 1 | id = $id; 17 | $this->cash = $cash; 18 | } 19 | 20 | public function getId(): int 21 | { 22 | return $this->id; 23 | } 24 | 25 | public function getCash(): string 26 | { 27 | return Utility::moneyToString($this->cash); 28 | } 29 | } -------------------------------------------------------------------------------- /src/UI/HomeController.php: -------------------------------------------------------------------------------- 1 | render('home.html.twig'); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Domain/Common/Handler.php: -------------------------------------------------------------------------------- 1 | getEventClass(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Domain/Common/AggregateRoot.php: -------------------------------------------------------------------------------- 1 | domainEvents; 18 | $this->domainEvents = []; 19 | 20 | return $events; 21 | } 22 | 23 | public function raise(DomainEvent $domainEvent): void 24 | { 25 | $this->domainEvents[] = $domainEvent; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Domain/SnackMachine/SnackMachineDto.php: -------------------------------------------------------------------------------- 1 | id = $id; 17 | $this->moneyInside = $moneyInside; 18 | } 19 | 20 | public function getId(): int 21 | { 22 | return $this->id; 23 | } 24 | 25 | public function getMoneyInside(): string 26 | { 27 | return Utility::moneyToString($this->moneyInside); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Infrastructure/Repository/DoctrineSnackRepository.php: -------------------------------------------------------------------------------- 1 | repository = $entityManager->getRepository(Snack::class); 17 | } 18 | 19 | public function find(int $id): Snack 20 | { 21 | return $this->repository->find($id); 22 | } 23 | } -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block stylesheets %} 8 | 9 | {% endblock %} 10 | 11 | DDD in Practice 12 | 13 | 14 | 15 |
16 |
17 |

{% block title %}{% endblock %}

18 | {% block body %}{% endblock %} 19 |
20 |
21 | 22 | {% block javascripts %}{% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 6 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 7 | Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true], 8 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 9 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 10 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 11 | ]; 12 | -------------------------------------------------------------------------------- /tests/unit/SnackPileTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidOperationException::class); 16 | new SnackPile(Snack::Chocolate(), -1, 0); 17 | } 18 | 19 | public function test_negative_price_throws_exception() 20 | { 21 | $this->expectException(InvalidOperationException::class); 22 | new SnackPile(Snack::Chocolate(), 0, -1); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | driver: 'pdo_sqlite' 4 | server_version: '5.7' 5 | charset: utf8mb4 6 | default_table_options: 7 | charset: utf8mb4 8 | collate: utf8mb4_unicode_ci 9 | 10 | url: 'sqlite:///%kernel.project_dir%/var/data.db' 11 | orm: 12 | auto_generate_proxy_classes: '%kernel.debug%' 13 | naming_strategy: doctrine.orm.naming_strategy.underscore 14 | auto_mapping: true 15 | mappings: 16 | App: 17 | is_bundle: false 18 | type: annotation 19 | dir: '%kernel.project_dir%/src/Domain' 20 | prefix: 'App\Domain' 21 | alias: App 22 | -------------------------------------------------------------------------------- /src/Domain/Management/BalanceChangedEventHandler.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 17 | } 18 | 19 | /** 20 | * @param BalanceChangedEvent $domainEvent 21 | */ 22 | public function handle($domainEvent): void 23 | { 24 | $headOffice = $this->repository->instance(); 25 | $headOffice->changeBalance($domainEvent->getDelta()); 26 | $this->repository->save($headOffice); 27 | } 28 | 29 | protected function getEventClass(): string 30 | { 31 | return BalanceChangedEvent::class; 32 | } 33 | } -------------------------------------------------------------------------------- /templates/home.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}Overview{% endblock %} 4 | 5 | {% block body %} 6 |
7 |

8 | This is a simple snack machine implemented in PHP and following the Domain Driven Design 9 | principles. Everything is based on the Pluralsight course 10 | Domain Driven Design in Practice 11 | by Vladimir Khorikov. 12 |

13 |

Click on the button:

14 |

15 | ATM 16 | Head Office 17 |

18 |
19 | {% endblock %} 20 | 21 | -------------------------------------------------------------------------------- /tests/unit/HeadOfficeTest.php: -------------------------------------------------------------------------------- 1 | loadMoney(Money::Dollar()); 18 | $atm = new Atm(); 19 | 20 | $headOffice->unloadCashFromSnackMachine($snackMachine); 21 | $headOffice->loadCashToAtm($atm); 22 | 23 | 24 | $this->assertEquals(0, $headOffice->getCash()->getAmount()); 25 | $this->assertEquals(0, $snackMachine->getMoneyInside()->getAmount()); 26 | $this->assertEquals(1, $atm->getMoneyInside()->getAmount()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Infrastructure/DomainHandlerCompilerPass.php: -------------------------------------------------------------------------------- 1 | has(DomainEventDispatcher::class)) { 16 | return; 17 | } 18 | 19 | $definition = $container->findDefinition(DomainEventDispatcher::class); 20 | 21 | $domainEventHandlers = $container->findTaggedServiceIds('app.domain_event_handler'); 22 | 23 | foreach ($domainEventHandlers as $id => $tags) { 24 | // Add handler to dispatcher 25 | $definition->addMethodCall('addHandler', array(new Reference($id))); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Infrastructure/Repository/DoctrineHeadOfficeRepository.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 20 | $this->repository = $this->entityManager->getRepository(HeadOffice::class); 21 | } 22 | 23 | public function instance(): HeadOffice 24 | { 25 | return $this->repository->find(self::HEAD_OFFICE_ID); 26 | } 27 | 28 | public function save(HeadOffice $headOffice): void 29 | { 30 | $this->entityManager->persist($headOffice); 31 | $this->entityManager->flush(); 32 | } 33 | } -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | metadata_cache_driver: 4 | type: service 5 | id: doctrine.system_cache_provider 6 | query_cache_driver: 7 | type: service 8 | id: doctrine.system_cache_provider 9 | result_cache_driver: 10 | type: service 11 | id: doctrine.result_cache_provider 12 | 13 | services: 14 | doctrine.result_cache_provider: 15 | class: Symfony\Component\Cache\DoctrineProvider 16 | public: false 17 | arguments: 18 | - '@doctrine.result_cache_pool' 19 | doctrine.system_cache_provider: 20 | class: Symfony\Component\Cache\DoctrineProvider 21 | public: false 22 | arguments: 23 | - '@doctrine.system_cache_pool' 24 | 25 | framework: 26 | cache: 27 | pools: 28 | doctrine.result_cache_pool: 29 | adapter: cache.app 30 | doctrine.system_cache_pool: 31 | adapter: cache.system 32 | -------------------------------------------------------------------------------- /src/Domain/Common/Entity.php: -------------------------------------------------------------------------------- 1 | id; 24 | } 25 | 26 | public function isEquals($other): bool 27 | { 28 | if ($other === null) { 29 | return false; 30 | } 31 | 32 | if ($this === $other) { 33 | return true; 34 | } 35 | 36 | if ( ! $other instanceof self) { 37 | return false; 38 | } 39 | 40 | if ($this->id === null || $other->id === null) { 41 | return false; 42 | } 43 | 44 | return $this->id === $other->id; 45 | } 46 | 47 | public function isNotEquals($other): bool 48 | { 49 | return ! $this->isEquals($other); 50 | } 51 | } -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | #default_locale: en 4 | #csrf_protection: true 5 | #http_method_override: true 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: ~ 11 | 12 | #esi: true 13 | #fragments: true 14 | php_errors: 15 | log: true 16 | 17 | cache: 18 | # Put the unique name of your app here: the prefix seed 19 | # is used to compute stable namespaces for cache keys. 20 | #prefix_seed: your_vendor_name/app_name 21 | 22 | # The app cache caches to the filesystem by default. 23 | # Other options include: 24 | 25 | # Redis 26 | #app: cache.adapter.redis 27 | #default_redis_provider: redis://localhost 28 | 29 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 30 | #app: cache.adapter.apcu 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Fabian Wüthrich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/Infrastructure/Repository/DoctrineAtmRepository.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 19 | $this->repository = $this->entityManager->getRepository(Atm::class); 20 | } 21 | 22 | public function find(int $id): Atm 23 | { 24 | return $this->repository->find($id); 25 | } 26 | 27 | public function findAll(): array 28 | { 29 | $atms = $this->repository->findAll(); 30 | 31 | return array_map(function (Atm $atm) { 32 | return new AtmDto($atm->getId(), $atm->getMoneyInside()->getAmount()); 33 | }, $atms); 34 | } 35 | 36 | public function save(Atm $atm): void 37 | { 38 | $this->entityManager->persist($atm); 39 | $this->entityManager->flush(); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Domain/SnackMachine/Slot.php: -------------------------------------------------------------------------------- 1 | snackMachine = $snackMachine; 30 | $this->position = $position; 31 | $this->snackPile = SnackPile::Empty(); 32 | } 33 | 34 | public function setSnackPile(SnackPile $snackPile): void 35 | { 36 | $this->snackPile = $snackPile; 37 | } 38 | 39 | public function getPosition(): int 40 | { 41 | return $this->position; 42 | } 43 | 44 | public function getSnackPile(): SnackPile 45 | { 46 | return $this->snackPile; 47 | } 48 | } -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: true #TODO Change this after upgrade to 4.1 6 | 7 | # Repository 8 | # You have to maintain this list manually in order to decouple doctrine from Domain 9 | App\Domain\SnackMachine\SnackMachineRepository: '@App\Infrastructure\Repository\DoctrineSnackMachineRepository' 10 | App\Domain\SnackMachine\SnackRepository: '@App\Infrastructure\Repository\DoctrineSnackRepository' 11 | App\Domain\Atm\AtmRepository: '@App\Infrastructure\Repository\DoctrineAtmRepository' 12 | App\Domain\Management\HeadOfficeRepository: '@App\Infrastructure\Repository\DoctrineHeadOfficeRepository' 13 | App\Domain\Atm\PaymentGateway: '@App\Infrastructure\PaymentGatewayStub' 14 | 15 | # Domain Event Handler 16 | App\Domain\Management\BalanceChangedEventHandler: 17 | tags: ['app.domain_event_handler'] 18 | 19 | App\Infrastructure\: 20 | resource: '../src/Infrastructure/*' 21 | exclude: '../src/Infrastructure/Migrations' 22 | 23 | App\Infrastructure\DomainEventDispatcher: 24 | tags: ['doctrine.event_subscriber'] 25 | 26 | App\UI\: 27 | resource: '../src/UI' 28 | tags: ['controller.service_arguments'] 29 | -------------------------------------------------------------------------------- /src/Domain/SnackMachine/Snack.php: -------------------------------------------------------------------------------- 1 | name = $name; 47 | $this->id = $id; 48 | } 49 | 50 | public function getName(): string 51 | { 52 | return $this->name; 53 | } 54 | 55 | public function __toString() 56 | { 57 | return $this->name; 58 | } 59 | } -------------------------------------------------------------------------------- /src/Infrastructure/Repository/DoctrineSnackMachineRepository.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 20 | $this->repository = $this->entityManager->getRepository(SnackMachine::class); 21 | } 22 | 23 | public function find(int $id): SnackMachine 24 | { 25 | return $this->repository->find($id); 26 | } 27 | 28 | public function findAll(): array 29 | { 30 | $snackMachines = $this->repository->findAll(); 31 | 32 | return array_map(function (SnackMachine $snackMachine) { 33 | return new SnackMachineDto($snackMachine->getId(), $snackMachine->getMoneyInside()->getAmount()); 34 | }, $snackMachines); 35 | } 36 | 37 | public function save(SnackMachine $snackMachine): void 38 | { 39 | $this->entityManager->persist($snackMachine); 40 | $this->entityManager->flush(); 41 | } 42 | } -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | load(__DIR__ . '/../.env'); 23 | } 24 | 25 | $input = new ArgvInput(); 26 | $env = $input->getParameterOption(['--env', '-e'], $_SERVER['APP_ENV'] ?? 'dev', true); 27 | $debug = ($_SERVER['APP_DEBUG'] ?? ('prod' !== $env)) && ! $input->hasParameterOption('--no-debug', true); 28 | 29 | if ($debug) { 30 | umask(0000); 31 | 32 | if (class_exists(Debug::class)) { 33 | Debug::enable(); 34 | } 35 | } 36 | 37 | $kernel = new Kernel($env, $debug); 38 | $application = new Application($kernel); 39 | $application->run($input); 40 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | load(__DIR__ . '/../.env'); 16 | } 17 | 18 | $env = $_SERVER['APP_ENV'] ?? 'dev'; 19 | $debug = $_SERVER['APP_DEBUG'] ?? ('prod' !== $env); 20 | 21 | if ($debug) { 22 | umask(0000); 23 | 24 | Debug::enable(); 25 | } 26 | 27 | if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { 28 | Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); 29 | } 30 | 31 | if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { 32 | Request::setTrustedHosts(explode(',', $trustedHosts)); 33 | } 34 | 35 | $kernel = new Kernel($env, $debug); 36 | $request = Request::createFromGlobals(); 37 | $response = $kernel->handle($request); 38 | $response->send(); 39 | $kernel->terminate($request, $response); 40 | -------------------------------------------------------------------------------- /src/Domain/Management/HeadOffice.php: -------------------------------------------------------------------------------- 1 | balance = 0.0; 25 | $this->cash = Money::None(); 26 | } 27 | 28 | public function changeBalance(float $delta): void 29 | { 30 | $this->balance += $delta; 31 | } 32 | 33 | public function unloadCashFromSnackMachine(SnackMachine $snackMachine): void 34 | { 35 | $money = $snackMachine->unloadMoney(); 36 | $this->cash = $this->cash->add($money); 37 | } 38 | 39 | public function loadCashToAtm(Atm $atm): void 40 | { 41 | $atm->loadMoney($this->cash); 42 | $this->cash = Money::None(); 43 | } 44 | 45 | public function getBalance(): float 46 | { 47 | return $this->balance; 48 | } 49 | 50 | public function getBalanceAsString(): string 51 | { 52 | return Utility::moneyToString($this->balance); 53 | } 54 | 55 | public function getCash(): Money 56 | { 57 | return $this->cash; 58 | } 59 | } -------------------------------------------------------------------------------- /templates/atm.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}ATM{% endblock %} 4 | 5 | {% block body %} 6 |
Money inside: {{ atm.moneyInside }}
7 |
Money charged: {{ atm.moneyChargedAsString }}
8 | 9 |
10 |

11 | {{ atm.moneyInside.oneCentCount }} 12 | {{ atm.moneyInside.tenCentCount }} 13 | {{ atm.moneyInside.quarterCount }} 14 |

15 |

16 | {{ atm.moneyInside.oneDollarCount }} 17 | {{ atm.moneyInside.fiveDollarCount }} 18 | {{ atm.moneyInside.twentyDollarCount }} 19 |

20 |
21 |
22 |
23 | 24 |
25 | 26 |
27 | {% for message in app.flashes('info') %} 28 |
29 |
30 | {{ message }} 31 |
32 |
33 | {% endfor %} 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fabwu/dddinaction", 3 | "description": "PHP implementation of the DDD in Practice Pluralsight course.", 4 | "license": "mit", 5 | "type": "project", 6 | "require": { 7 | "php": "^7.1.3", 8 | "ext-iconv": "*", 9 | "sensio/framework-extra-bundle": "^5.1", 10 | "symfony/console": "^4.0", 11 | "symfony/flex": "^1.0", 12 | "symfony/framework-bundle": "^4.0", 13 | "symfony/orm-pack": "^1.0", 14 | "symfony/proxy-manager-bridge": "^4.0", 15 | "symfony/twig-bundle": "^4.0", 16 | "symfony/yaml": "^4.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^6.5", 20 | "symfony/dotenv": "^4.0", 21 | "symfony/maker-bundle": "^1.4" 22 | }, 23 | "config": { 24 | "preferred-install": { 25 | "*": "dist" 26 | }, 27 | "sort-packages": true 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "App\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "App\\Tests\\": "tests/" 37 | } 38 | }, 39 | "replace": { 40 | "symfony/polyfill-iconv": "*", 41 | "symfony/polyfill-php71": "*", 42 | "symfony/polyfill-php70": "*", 43 | "symfony/polyfill-php56": "*" 44 | }, 45 | "scripts": { 46 | "auto-scripts": { 47 | "cache:clear": "symfony-cmd", 48 | "assets:install --symlink --relative %PUBLIC_DIR%": "symfony-cmd" 49 | }, 50 | "post-install-cmd": [ 51 | "@auto-scripts" 52 | ], 53 | "post-update-cmd": [ 54 | "@auto-scripts" 55 | ] 56 | }, 57 | "conflict": { 58 | "symfony/symfony": "*" 59 | }, 60 | "extra": { 61 | "symfony": { 62 | "id": "01C7X1X3321YRXN6VGCMGB7KTF", 63 | "allow-contrib": false 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Infrastructure/Migrations/Version20180502180448.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); 17 | 18 | $this->addSql('CREATE TABLE snack_machine (id INTEGER NOT NULL, money_in_transaction_one_cent_count INTEGER NOT NULL, money_in_transaction_ten_cent_count INTEGER NOT NULL, money_in_transaction_quarter_count INTEGER NOT NULL, money_in_transaction_one_dollar_count INTEGER NOT NULL, money_in_transaction_five_dollar_count INTEGER NOT NULL, money_in_transaction_twenty_dollar_count INTEGER NOT NULL, money_inside_one_cent_count INTEGER NOT NULL, money_inside_ten_cent_count INTEGER NOT NULL, money_inside_quarter_count INTEGER NOT NULL, money_inside_one_dollar_count INTEGER NOT NULL, money_inside_five_dollar_count INTEGER NOT NULL, money_inside_twenty_dollar_count INTEGER NOT NULL, PRIMARY KEY(id))'); 19 | } 20 | 21 | public function down(Schema $schema) 22 | { 23 | // this down() migration is auto-generated, please modify it to your needs 24 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); 25 | 26 | $this->addSql('DROP TABLE snack_machine'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/unit/AtmTest.php: -------------------------------------------------------------------------------- 1 | loadMoney(Money::Dollar()); 15 | 16 | $atm->takeMoney(1); 17 | 18 | $this->assertEquals(0, $atm->getMoneyInside()->getAmount()); 19 | $this->assertEquals(1.01, $atm->getMoneyCharged()); 20 | } 21 | 22 | public function test_commission_is_at_least_one_cent(): void 23 | { 24 | $atm = new Atm(); 25 | $atm->loadMoney(Money::Cent()); 26 | 27 | $atm->takeMoney(0.01); 28 | 29 | $this->assertEquals(0.02, $atm->getMoneyCharged()); 30 | } 31 | 32 | public function test_commission_is_rounded_up_to_next_cent(): void 33 | { 34 | $atm = new Atm(); 35 | $atm->loadMoney(Money::Dollar()); 36 | $atm->loadMoney(Money::TenCent()); 37 | 38 | $atm->takeMoney(1.1); 39 | 40 | $this->assertEquals(1.12, $atm->getMoneyCharged()); 41 | } 42 | 43 | public function test_zero_amount(): void 44 | { 45 | $atm = new Atm(); 46 | 47 | $this->expectExceptionMessage('Invalid amount'); 48 | 49 | $atm->takeMoney(0); 50 | } 51 | 52 | public function test_not_enough_money(): void 53 | { 54 | $atm = new Atm(); 55 | $atm->loadMoney(Money::Dollar()); 56 | 57 | $this->expectExceptionMessage('Not enough money'); 58 | 59 | $atm->takeMoney(2); 60 | } 61 | 62 | public function test_not_enough_change(): void 63 | { 64 | $atm = new Atm(); 65 | $atm->loadMoney(Money::Dollar()); 66 | 67 | $this->expectExceptionMessage('Not enough change'); 68 | 69 | $atm->takeMoney(0.5); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/UI/AtmController.php: -------------------------------------------------------------------------------- 1 | atmRepository = $atmRepository; 27 | $this->paymentGateway = $paymentGateway; 28 | } 29 | 30 | /** 31 | * @Route("/{id}", name="atm-overview") 32 | */ 33 | public function overview(int $id): Response 34 | { 35 | return $this->render('atm.html.twig', [ 36 | 'atm' => $this->atmRepository->find($id), 37 | ]); 38 | } 39 | 40 | /** 41 | * @Route("/{id}/take-money", name="atm-take-money") 42 | */ 43 | public function takeMoney(int $id, Request $request): Response 44 | { 45 | $amount = $request->get('amount'); 46 | $atm = $this->atmRepository->find($id); 47 | 48 | try { 49 | $amountWithCommission = $atm->calculateAmountWithCommission($amount); 50 | $this->paymentGateway->chargePayment($amountWithCommission); 51 | $atm->takeMoney((float)$amount); 52 | $this->atmRepository->save($atm); 53 | $msg = 'You have taken ' . Utility::moneyToString($amount); 54 | } catch (InvalidOperationException $e) { 55 | $msg = $e->getMessage(); 56 | } 57 | 58 | $this->addFlash('info', $msg); 59 | 60 | return $this->redirectToRoute('atm-overview', ['id' => $atm->getId()]); 61 | } 62 | } -------------------------------------------------------------------------------- /tests/integration/IntegrationTest.php: -------------------------------------------------------------------------------- 1 | get(SnackMachineRepository::class) 20 | ->find(1); 21 | 22 | $this->assertNotNull($snackMachine); 23 | } 24 | 25 | public function test_snack_reference_data(): void 26 | { 27 | $repository = parent::$container->get(SnackRepository::class); 28 | $chocolate = $repository->find(Snack::Chocolate()->getId()); 29 | $soda = $repository->find(Snack::Soda()->getId()); 30 | $gum = $repository->find(Snack::Gum()->getId()); 31 | 32 | $this->assertEquals($chocolate->getName(), Snack::Chocolate()->getName()); 33 | $this->assertEquals($soda->getName(), Snack::Soda()->getName()); 34 | $this->assertEquals($gum->getName(), Snack::Gum()->getName()); 35 | } 36 | 37 | public function test_atm_repository(): void 38 | { 39 | $atm = parent::$container 40 | ->get(AtmRepository::class) 41 | ->find(1); 42 | 43 | $this->assertNotNull($atm); 44 | } 45 | 46 | public function test_head_office_repository(): void 47 | { 48 | $headOffice = parent::$container 49 | ->get(HeadOfficeRepository::class) 50 | ->instance(); 51 | 52 | $this->assertNotNull($headOffice); 53 | } 54 | 55 | public static $class = Kernel::class; 56 | 57 | protected function setUp() 58 | { 59 | parent::bootKernel(); 60 | } 61 | 62 | protected function tearDown() 63 | { 64 | parent::tearDown(); 65 | } 66 | } -------------------------------------------------------------------------------- /templates/head-office.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}Head Office{% endblock %} 4 | 5 | {% block body %} 6 |
Balance: {{ headOffice.balanceAsString }}
7 |
Cash: {{ headOffice.cash }}
8 | 9 |
10 |

Snack Machines

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for snackMachine in snackMachines %} 21 | 22 | 23 | 24 | 28 | 29 | {% endfor %} 30 | 31 |
IDMoney Inside
{{ snackMachine.id }}{{ snackMachine.moneyInside }} 25 | Show | 26 | Unload cash 27 |
32 |
33 | 34 |
35 |

Atms

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {% for atm in atms %} 46 | 47 | 48 | 49 | 53 | 54 | {% endfor %} 55 | 56 |
IDCash
{{ atm.id }}{{ atm.cash }} 50 | Show | 51 | Load Cash 52 |
57 |
58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /src/Domain/SnackMachine/SnackPile.php: -------------------------------------------------------------------------------- 1 | snackId = $snack->getId(); 38 | $this->quantity = $quantity; 39 | $this->price = $price; 40 | } 41 | 42 | public function subtractOne(): SnackPile 43 | { 44 | $oldSnackId = $this->snackId; 45 | $newSnackPile = new SnackPile(Snack::None(), $this->quantity - 1, $this->price); 46 | $newSnackPile->snackId = $oldSnackId; 47 | 48 | return $newSnackPile; 49 | } 50 | 51 | public function getSnack(): Snack 52 | { 53 | $snackArray = array_filter(Snack::getAllSnacks(), function (Snack $snack) { 54 | return $snack->getId() === $this->snackId; 55 | }); 56 | 57 | return array_shift($snackArray); 58 | } 59 | 60 | public function getQuantity(): int 61 | { 62 | return $this->quantity; 63 | } 64 | 65 | public function getPrice(): float 66 | { 67 | return $this->price; 68 | } 69 | 70 | public function isEquals($obj): bool 71 | { 72 | return 73 | $obj instanceof self && 74 | $this->snackId === $obj->snackId && 75 | $this->quantity === $obj->quantity && 76 | $this->price === $obj->price; 77 | } 78 | } -------------------------------------------------------------------------------- /src/Infrastructure/DomainEventDispatcher.php: -------------------------------------------------------------------------------- 1 | handlers[] = $handler; 27 | } 28 | 29 | public function getSubscribedEvents(): array 30 | { 31 | return [ 32 | 'postPersist', 33 | 'postUpdate', 34 | 'postRemove', 35 | 'postFlush', 36 | ]; 37 | } 38 | 39 | public function postPersist(LifecycleEventArgs $event): void 40 | { 41 | $this->keepAggregateRoots($event); 42 | } 43 | 44 | public function postUpdate(LifecycleEventArgs $event): void 45 | { 46 | $this->keepAggregateRoots($event); 47 | } 48 | 49 | public function postRemove(LifecycleEventArgs $event): void 50 | { 51 | $this->keepAggregateRoots($event); 52 | } 53 | 54 | public function postFlush(PostFlushEventArgs $flushEvent): void 55 | { 56 | foreach ($this->entities as $entity) { 57 | $events = $entity->popEvents(); 58 | 59 | foreach ($events as $event) { 60 | foreach ($this->handlers as $handler) { 61 | if ($handler->canHandle($event)) { 62 | $handler->handle($event); 63 | } 64 | } 65 | } 66 | } 67 | 68 | $this->entities = []; 69 | } 70 | 71 | private function keepAggregateRoots(LifecycleEventArgs $event): void 72 | { 73 | $entity = $event->getObject(); 74 | 75 | if ( ! ($entity instanceof AggregateRoot)) { 76 | return; 77 | } 78 | 79 | $this->entities[] = $entity; 80 | } 81 | } -------------------------------------------------------------------------------- /src/UI/HeadOfficeController.php: -------------------------------------------------------------------------------- 1 | headOfficeRepository = $headOfficeRepository; 29 | $this->snackMachineRepository = $snackMachineRepository; 30 | $this->atmRepository = $atmRepository; 31 | } 32 | 33 | /** 34 | * @Route("/", name="head-office-overview") 35 | */ 36 | public function overview(): Response 37 | { 38 | return $this->render('head-office.html.twig', [ 39 | 'headOffice' => $this->headOfficeRepository->instance(), 40 | 'snackMachines' => $this->snackMachineRepository->findAll(), 41 | 'atms' => $this->atmRepository->findAll(), 42 | ]); 43 | } 44 | 45 | /** 46 | * @Route("/unload-cash/{id}", name="head-office-unload-cash") 47 | */ 48 | public function unloadCash(int $id) 49 | { 50 | $headOffice = $this->headOfficeRepository->instance(); 51 | $snackMachine = $this->snackMachineRepository->find($id); 52 | 53 | $headOffice->unloadCashFromSnackMachine($snackMachine); 54 | 55 | $this->headOfficeRepository->save($headOffice); 56 | $this->snackMachineRepository->save($snackMachine); 57 | 58 | return $this->redirectToRoute('head-office-overview'); 59 | } 60 | 61 | /** 62 | * @Route("/load-cash/{id}", name="head-office-load-cash") 63 | */ 64 | public function loadCash(int $id) 65 | { 66 | $headOffice = $this->headOfficeRepository->instance(); 67 | $atm = $this->atmRepository->find($id); 68 | 69 | $headOffice->loadCashToAtm($atm); 70 | 71 | $this->headOfficeRepository->save($headOffice); 72 | $this->atmRepository->save($atm); 73 | 74 | return $this->redirectToRoute('head-office-overview'); 75 | } 76 | } -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | getProjectDir() . '/var/cache/' . $this->environment; 21 | } 22 | 23 | public function getLogDir() 24 | { 25 | return $this->getProjectDir() . '/var/log'; 26 | } 27 | 28 | protected function build(ContainerBuilder $container) 29 | { 30 | $container->addCompilerPass(new DomainHandlerCompilerPass()); 31 | } 32 | 33 | public function registerBundles() 34 | { 35 | $contents = require $this->getProjectDir() . '/config/bundles.php'; 36 | foreach ($contents as $class => $envs) { 37 | if (isset($envs['all']) || isset($envs[$this->environment])) { 38 | yield new $class(); 39 | } 40 | } 41 | } 42 | 43 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader) 44 | { 45 | // Feel free to remove the "container.autowiring.strict_mode" parameter 46 | // if you are using symfony/dependency-injection 4.0+ as it's the default behavior 47 | $container->setParameter('container.autowiring.strict_mode', true); 48 | $container->setParameter('container.dumper.inline_class_loader', true); 49 | $confDir = $this->getProjectDir() . '/config'; 50 | 51 | $loader->load($confDir . '/{packages}/*' . self::CONFIG_EXTS, 'glob'); 52 | $loader->load($confDir . '/{packages}/' . $this->environment . '/**/*' . self::CONFIG_EXTS, 'glob'); 53 | $loader->load($confDir . '/{services}' . self::CONFIG_EXTS, 'glob'); 54 | $loader->load($confDir . '/{services}_' . $this->environment . self::CONFIG_EXTS, 'glob'); 55 | } 56 | 57 | protected function configureRoutes(RouteCollectionBuilder $routes) 58 | { 59 | $confDir = $this->getProjectDir() . '/config'; 60 | 61 | $routes->import($confDir . '/{routes}/*' . self::CONFIG_EXTS, '/', 'glob'); 62 | $routes->import($confDir . '/{routes}/' . $this->environment . '/**/*' . self::CONFIG_EXTS, '/', 'glob'); 63 | $routes->import($confDir . '/{routes}' . self::CONFIG_EXTS, '/', 'glob'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Domain/Atm/Atm.php: -------------------------------------------------------------------------------- 1 | moneyInside = Money::None(); 30 | $this->moneyCharged = 0; 31 | } 32 | 33 | public function takeMoney(float $amount): void 34 | { 35 | $this->canTakeMoney($amount); 36 | 37 | $output = $this->moneyInside->allocate($amount); 38 | $this->moneyInside = $this->moneyInside->sub($output); 39 | 40 | $amountWithCommission = $this->calculateAmountWithCommission($amount); 41 | $this->moneyCharged += $amountWithCommission; 42 | 43 | $this->raise(new BalanceChangedEvent($amountWithCommission)); 44 | } 45 | 46 | private function canTakeMoney(float $amount): void 47 | { 48 | if ($amount <= 0) { 49 | throw new InvalidOperationException('Invalid amount'); 50 | } 51 | 52 | if ($this->moneyInside->getAmount() < $amount) { 53 | throw new InvalidOperationException('Not enough money'); 54 | } 55 | 56 | if ( ! $this->moneyInside->allocate($amount)->getAmount()) { 57 | throw new InvalidOperationException('Not enough change'); 58 | } 59 | } 60 | 61 | public function calculateAmountWithCommission(float $amount): float 62 | { 63 | $commission = $amount * self::COMMISSION_RATE; 64 | $lessThanCent = fmod($commission, 0.01); 65 | if ($lessThanCent > 0) { 66 | $commission = $commission - $lessThanCent + 0.01; 67 | } 68 | 69 | return $amount + $commission; 70 | } 71 | 72 | public function loadMoney(Money $money): void 73 | { 74 | $this->moneyInside = $this->moneyInside->add($money); 75 | } 76 | 77 | public function getMoneyInside(): Money 78 | { 79 | return $this->moneyInside; 80 | } 81 | 82 | public function getMoneyCharged(): float 83 | { 84 | return $this->moneyCharged; 85 | } 86 | 87 | public function getMoneyChargedAsString(): string 88 | { 89 | return Utility::moneyToString($this->moneyCharged); 90 | } 91 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DDD Snack Machine 2 | 3 | [![Build Status](https://travis-ci.com/fabwu/dddinaction.svg?branch=module-7)](https://travis-ci.com/fabwu/dddinaction) 4 | 5 | This is a simple snack machine implemented in PHP and following the Domain Driven Design 6 | principles. Everything is based on the Pluralsight course 7 | [Domain Driven Design in Practice](https://www.pluralsight.com/courses/domain-driven-design-in-practice). 8 | You can find the original C# source code [here](https://github.com/vkhorikov/DddInAction). 9 | 10 | The repository contains the following branches each reflecting a module in the course: 11 | 12 | - `module-2` Starting with the First Bounded Context 13 | - `module-3` Introducing UI and Persistence Layers 14 | - `module-5` Extending the Bounded Context with Aggregates & Introducing Repositories 15 | - `module-6` Introducing the Second Bounded Context 16 | - `module-7` Working with Domain Events 17 | 18 | I couldn't implement everything exactly like Vladimir suggested it. Here is a list of 19 | things I had to adapt for the PHP language: 20 | 21 | - I stored the snack machine with both money value types in the database. As a first 22 | approach I used sessions to overcome the state problem but this seems a bit like a overkill 23 | so I stored everything in the database. Therefore, I have to read and save the snack machine 24 | after each action but I couldn't come up with a better solution. 25 | - I didn't have to break encapsulation or add new constructors because Doctrine discover 26 | the properties via reflection and can create the proxies. 27 | - I had to use annotations to describe the orm mapping. I don't really like them because they 28 | clutter up my entities with persistence logic but the other options (xml, yml, php) don't provide 29 | refactoring or auto-completion. 30 | - Doctrine doesn't support entities on embeddables so I just use the id as an integer column. 31 | - The domain events implementation is inspired from [this blogpost](https://beberlei.de/2013/07/24/doctrine_and_domainevents.html). 32 | I tried to use a more type save approach but it's still ugly due to the php type system. You also have 33 | to register all domain events handler manually. A compiler pass collects all tagged handlers and adds them 34 | to the dispatcher. 35 | 36 | I also tried to encapsulate the modules via [composer's path setting](https://getcomposer.org/doc/05-repositories.md#path). This was not useful 37 | because it provides no encapsulation (e.g. Domain module can access UI module) and slows 38 | down development. 39 | 40 | ## Installation 41 | 42 | Run the following commands to install and start the server. 43 | ``` 44 | composer install 45 | php bin/console doctrine:migrations:migrate 46 | php -S localhost:8080 -t public/ 47 | ``` 48 | You can then access the application under http://localhost:8080 49 | 50 | # Testing 51 | 52 | Run the tests with the following command: 53 | ``` 54 | # Unit Tests 55 | vendor/bin/phpunit --configuration phpunit.xml --testsuite unit 56 | 57 | # Integration Tests 58 | vendor/bin/phpunit --configuration phpunit.xml --testsuite integration 59 | ``` -------------------------------------------------------------------------------- /templates/snack-machine.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}Snack Machine{% endblock %} 4 | 5 | {% block body %} 6 |
7 | {% for snackPile in snackMachine.allSnackPiles %} 8 |
9 | 10 |

11 | ${{ snackPile.price|number_format(2) }}
12 | {{ snackPile.quantity }} left 13 |

14 |
15 | {% endfor %} 16 |
17 |
18 |
19 | Buy #1 20 | Buy #2 21 | Buy #3 22 |
23 |

24 | Money inserted: {{ snackMachine.moneyInTransactionAsString }} 25 |

26 |
27 | Put ¢1 28 | Put ¢10 29 | Put ¢25 30 |
31 |
32 | Put $1 33 | Put $5 34 | Put $20 35 |
36 |
37 | Return money 38 |
39 |
40 |
41 |

Money inside: {{ snackMachine.moneyInside }}

42 |

43 | {{ snackMachine.moneyInside.oneCentCount }} 44 | {{ snackMachine.moneyInside.tenCentCount }} 45 | {{ snackMachine.moneyInside.quarterCount }} 46 |

47 |

48 | {{ snackMachine.moneyInside.oneDollarCount }} 49 | {{ snackMachine.moneyInside.fiveDollarCount }} 50 | {{ snackMachine.moneyInside.twentyDollarCount }} 51 |

52 |
53 | {% for message in app.flashes('info') %} 54 |
55 |
56 | {{ message }} 57 |
58 |
59 | {% endfor %} 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /src/UI/SnackMachineController.php: -------------------------------------------------------------------------------- 1 | snackMachineRepository = $snackMachineRepository; 26 | } 27 | 28 | /** 29 | * @Route("/{id}", name="snack-machine-overview") 30 | */ 31 | public function overview($id): Response 32 | { 33 | return $this->render('snack-machine.html.twig', [ 34 | 'snackMachine' => $this->snackMachineRepository->find($id), 35 | ]); 36 | } 37 | 38 | /** 39 | * @Route("/{id}/insert-money/{amount}", name="snack-machine-insert-money") 40 | */ 41 | public function insertMoney(int $id, string $amount) 42 | { 43 | $money = $this->getMoney($amount); 44 | $snackMachine = $this->snackMachineRepository->find($id); 45 | 46 | try { 47 | $snackMachine->insertMoney($money); 48 | $this->snackMachineRepository->save($snackMachine); 49 | $message = Utility::moneyToString($money->getAmount()) . ' inserted'; 50 | } catch (InvalidOperationException $e) { 51 | $message = $e->getMessage(); 52 | } 53 | 54 | $this->addFlash('info', $message); 55 | 56 | return $this->redirectToRoute('snack-machine-overview', ['id' => $snackMachine->getId()]); 57 | } 58 | 59 | /** 60 | * @Route("/{id}/buy-snack/{position}", name="snack-machine-buy-snack") 61 | */ 62 | public function buySnack(int $id, int $position) 63 | { 64 | $snackMachine = $this->snackMachineRepository->find($id); 65 | 66 | try { 67 | $snackMachine->buySnack($position); 68 | $this->snackMachineRepository->save($snackMachine); 69 | $message = 'Snack #' . $position . ' bought'; 70 | } catch (InvalidOperationException $e) { 71 | $message = $e->getMessage(); 72 | } 73 | 74 | $this->addFlash('info', $message); 75 | 76 | return $this->redirectToRoute('snack-machine-overview', ['id' => $snackMachine->getId()]); 77 | } 78 | 79 | /** 80 | * @Route("/{id}/return-money", name="snack-machine-return-money") 81 | */ 82 | public function returnMoney(int $id) 83 | { 84 | $snackMachine = $this->snackMachineRepository->find($id); 85 | 86 | try { 87 | $money = $snackMachine->returnMoney(); 88 | $this->snackMachineRepository->save($snackMachine); 89 | $message = Utility::moneyToString($money->getAmount()) . ' returned'; 90 | } catch (InvalidOperationException $e) { 91 | $message = $e->getMessage(); 92 | } 93 | 94 | $this->addFlash('info', $message); 95 | 96 | return $this->redirectToRoute('snack-machine-overview', ['id' => $snackMachine->getId()]); 97 | } 98 | 99 | private function getMoney($amount): Money 100 | { 101 | switch ($amount) { 102 | case 'one-cent': 103 | $money = Money::Cent(); 104 | break; 105 | case 'ten-cent': 106 | $money = Money::TenCent(); 107 | break; 108 | case 'quarter': 109 | $money = Money::Quarter(); 110 | break; 111 | case 'one-dollar': 112 | $money = Money::Dollar(); 113 | break; 114 | case 'five-dollar': 115 | $money = Money::FiveDollar(); 116 | break; 117 | case 'twenty-dollar': 118 | $money = Money::TwentyDollar(); 119 | break; 120 | default: 121 | throw new RuntimeException('You cannot insert this money'); 122 | } 123 | 124 | return $money; 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/Domain/SnackMachine/SnackMachine.php: -------------------------------------------------------------------------------- 1 | moneyInTransaction = 0; 38 | $this->moneyInside = Money::None(); 39 | $this->slots = new ArrayCollection([ 40 | new Slot($this, 1), 41 | new Slot($this, 2), 42 | new Slot($this, 3), 43 | ]); 44 | } 45 | 46 | public function insertMoney(Money $money): void 47 | { 48 | if ( 49 | Money::Cent()->isNotEquals($money) && 50 | Money::TenCent()->isNotEquals($money) && 51 | Money::Quarter()->isNotEquals($money) && 52 | Money::Dollar()->isNotEquals($money) && 53 | Money::FiveDollar()->isNotEquals($money) && 54 | Money::TwentyDollar()->isNotEquals($money) 55 | ) { 56 | throw new LogicException('You can only insert one coin at a time'); 57 | } 58 | 59 | $this->moneyInTransaction += $money->getAmount(); 60 | $this->moneyInside = $this->moneyInside->add($money); 61 | } 62 | 63 | public function returnMoney(): Money 64 | { 65 | $moneyToReturn = $this->moneyInside->allocate($this->moneyInTransaction); 66 | $this->moneyInside = $this->moneyInside->sub($moneyToReturn); 67 | $this->moneyInTransaction = 0; 68 | 69 | return $moneyToReturn; 70 | } 71 | 72 | public function buySnack(int $position): void 73 | { 74 | $slot = $this->getSlot($position); 75 | 76 | if ($slot->getSnackPile()->getPrice() > $this->moneyInTransaction) { 77 | throw new InvalidOperationException('Not enough money in transaction'); 78 | } 79 | 80 | $slot->setSnackPile($slot->getSnackPile()->subtractOne()); 81 | 82 | $changeAmount = $this->moneyInTransaction - $slot->getSnackPile()->getPrice(); 83 | $change = $this->moneyInside->allocate($changeAmount); 84 | 85 | if ($change->getAmount() < $changeAmount) { 86 | throw new InvalidOperationException('Not enough change inside the snack machine'); 87 | } 88 | 89 | $this->moneyInTransaction = 0; 90 | 91 | $this->moneyInside = $this->moneyInside->sub($change); 92 | } 93 | 94 | public function getMoneyInTransaction(): float 95 | { 96 | return $this->moneyInTransaction; 97 | } 98 | 99 | public function getMoneyInTransactionAsString(): string 100 | { 101 | return Utility::moneyToString($this->moneyInTransaction); 102 | } 103 | 104 | public function getMoneyInside(): Money 105 | { 106 | return $this->moneyInside; 107 | } 108 | 109 | public function getSnackPile(int $position): SnackPile 110 | { 111 | return $this->getSlot($position)->getSnackPile(); 112 | } 113 | 114 | public function getAllSnackPiles(): array 115 | { 116 | return $this->slots 117 | ->map(function (Slot $slot) { 118 | return $slot->getSnackPile(); 119 | }) 120 | ->getValues(); 121 | } 122 | 123 | public function loadSnack(int $position, SnackPile $snackPile): void 124 | { 125 | $slot = $this->getSlot($position); 126 | $slot->setSnackPile($snackPile); 127 | } 128 | 129 | public function loadMoney(Money $money): void 130 | { 131 | $this->moneyInside = $this->moneyInside->add($money); 132 | } 133 | 134 | public function unloadMoney(): Money 135 | { 136 | $unloadedMoney = $this->moneyInside; 137 | $this->moneyInside = Money::None(); 138 | 139 | return $unloadedMoney; 140 | } 141 | 142 | private function getSlot(int $position): Slot 143 | { 144 | return $this->slots 145 | ->filter(function (Slot $slot) use ($position) { 146 | return $slot->getPosition() === $position; 147 | }) 148 | ->first(); 149 | } 150 | } -------------------------------------------------------------------------------- /tests/unit/MoneyTest.php: -------------------------------------------------------------------------------- 1 | isEquals($money2); 18 | 19 | $this->assertTrue($isEquals); 20 | } 21 | 22 | public function testTwoMoneyWithDifferentAmountAreNotEqual() 23 | { 24 | $money1 = new Money(1, 2, 3, 4, 5, 6); 25 | $money2 = new Money(1, 2, 100, 4, 5, 6); 26 | 27 | $isEquals = $money1->isEquals($money2); 28 | 29 | $this->assertFalse($isEquals); 30 | } 31 | 32 | public function testSumOfTwoMoneyReturnNewMoney() 33 | { 34 | $money1 = new Money(1, 2, 3, 4, 5, 6); 35 | $money2 = new Money(1, 2, 3, 4, 5, 6); 36 | 37 | $sum = $money1->add($money2); 38 | 39 | $this->assertEquals(2, $sum->getOneCentCount()); 40 | $this->assertEquals(4, $sum->getTenCentCount()); 41 | $this->assertEquals(6, $sum->getQuarterCount()); 42 | $this->assertEquals(8, $sum->getOneDollarCount()); 43 | $this->assertEquals(10, $sum->getFiveDollarCount()); 44 | $this->assertEquals(12, $sum->getTwentyDollarCount()); 45 | } 46 | 47 | public function testSubstractionOfTwoMoneysProducesCorrectResult() 48 | { 49 | $money1 = new Money(10, 10, 10, 10, 10, 10); 50 | $money2 = new Money(1, 2, 3, 4, 5, 6); 51 | 52 | $result = $money1->sub($money2); 53 | 54 | $this->assertEquals(9, $result->getOneCentCount()); 55 | $this->assertEquals(8, $result->getTenCentCount()); 56 | $this->assertEquals(7, $result->getQuarterCount()); 57 | $this->assertEquals(6, $result->getOneDollarCount()); 58 | $this->assertEquals(5, $result->getFiveDollarCount()); 59 | $this->assertEquals(4, $result->getTwentyDollarCount()); 60 | } 61 | 62 | public function testShouldNotSubstractMoreThanExists() 63 | { 64 | $money1 = new Money(0, 1, 0, 0, 0, 0); 65 | $money2 = new Money(1, 0, 0, 0, 0, 0); 66 | 67 | $this->expectException(LogicException::class); 68 | 69 | $money1->sub($money2); 70 | } 71 | 72 | /** 73 | * @dataProvider amountProvider 74 | */ 75 | public function testAmountCalculatedCorrectly($a, $b, $c, $d, $e, $f, $amount) 76 | { 77 | $money = new Money($a, $b, $c, $d, $e, $f); 78 | 79 | $this->assertEquals($amount, $money->getAmount()); 80 | } 81 | 82 | public function amountProvider() 83 | { 84 | return [ 85 | [0, 0, 0, 0, 0, 0, 0], 86 | [1, 0, 0, 0, 0, 0, 0.01], 87 | [1, 2, 0, 0, 0, 0, 0.21], 88 | [1, 2, 3, 0, 0, 0, 0.96], 89 | [1, 2, 3, 4, 0, 0, 4.96], 90 | [1, 2, 3, 4, 5, 0, 29.96], 91 | [1, 2, 3, 4, 5, 6, 149.96], 92 | [11, 0, 0, 0, 0, 0, 0.11], 93 | [110, 0, 0, 0, 100, 0, 501.1], 94 | ]; 95 | } 96 | 97 | /** 98 | * @dataProvider negativProvider 99 | */ 100 | public function testCantCreateMoneyWithNegativeCount($a, $b, $c, $d, $e, $f) 101 | { 102 | $this->expectException(LogicException::class); 103 | 104 | new Money($a, $b, $c, $d, $e, $f); 105 | } 106 | 107 | public function negativProvider() 108 | { 109 | return [ 110 | [-1, 0, 0, 0, 0, 0], 111 | [0, -1, 0, 0, 0, 0], 112 | [0, 0, -1, 0, 0, 0], 113 | [0, 0, 0, -1, 0, 0], 114 | [0, 0, 0, 0, -1, 0], 115 | [0, 0, 0, 0, 0, -1], 116 | ]; 117 | } 118 | 119 | public function test_money_allocates_highest_value() 120 | { 121 | $money = Money::Dollar(); 122 | $money = $money->add(Money::Quarter()); 123 | $money = $money->add(Money::Quarter()); 124 | $money = $money->add(Money::Quarter()); 125 | $money = $money->add(Money::Quarter()); 126 | 127 | $allocatedMoney = $money->allocate(1); 128 | 129 | $this->assertEquals(1, $allocatedMoney->getOneDollarCount()); 130 | $this->assertEquals(0, $allocatedMoney->getQuarterCount()); 131 | } 132 | 133 | public function test_strange_edge_case() 134 | { 135 | $money = Money::Cent(); 136 | $money = $money->add(Money::TenCent()); 137 | 138 | $allocatedMoney = $money->allocate(0.11); 139 | 140 | $this->assertEquals(1, $allocatedMoney->getOneCentCount()); 141 | $this->assertEquals(1, $allocatedMoney->getTenCentCount()); 142 | } 143 | 144 | public function test_to_string() 145 | { 146 | $oneDollarAndTenCents = Money::Dollar()->add(Money::TenCent()); 147 | 148 | $this->assertEquals('$ 1.10', (string)$oneDollarAndTenCents); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Infrastructure/Migrations/Version20180516161021.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); 17 | 18 | $this->addSql('CREATE TABLE slot (id INTEGER NOT NULL, snack_machine_id INTEGER DEFAULT NULL, position INTEGER NOT NULL, snack_pile_snack_id INTEGER NOT NULL, snack_pile_quantity INTEGER NOT NULL, snack_pile_price DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); 19 | $this->addSql('CREATE INDEX IDX_AC0E2067B0081B53 ON slot (snack_machine_id)'); 20 | $this->addSql('CREATE TABLE snack (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); 21 | $this->addSql('CREATE TEMPORARY TABLE __temp__snack_machine AS SELECT id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM snack_machine'); 22 | $this->addSql('DROP TABLE snack_machine'); 23 | $this->addSql('CREATE TABLE snack_machine (id INTEGER NOT NULL, money_inside_one_cent_count INTEGER NOT NULL, money_inside_ten_cent_count INTEGER NOT NULL, money_inside_quarter_count INTEGER NOT NULL, money_inside_one_dollar_count INTEGER NOT NULL, money_inside_five_dollar_count INTEGER NOT NULL, money_inside_twenty_dollar_count INTEGER NOT NULL, money_in_transaction DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); 24 | $this->addSql('INSERT INTO snack_machine (id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count) SELECT id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM __temp__snack_machine'); 25 | $this->addSql('DROP TABLE __temp__snack_machine'); 26 | 27 | $this->addSql('INSERT INTO snack_machine(id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count, money_in_transaction) 28 | VALUES (1,0,0,0,0,0,0,0)'); 29 | $this->addSql("INSERT INTO snack(id, name) VALUES (1, 'Chocolate'), (2, 'Soda'), (3, 'Gum')"); 30 | $this->addSql('INSERT INTO slot(id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price) VALUES 31 | (1, 1, 1, 1 ,10, 1.00), 32 | (2, 1, 2, 2, 22, 2.00), 33 | (3, 1, 3, 3, 14, 3.00) 34 | '); 35 | } 36 | 37 | public function down(Schema $schema) 38 | { 39 | // this down() migration is auto-generated, please modify it to your needs 40 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); 41 | 42 | $this->addSql('DROP TABLE slot'); 43 | $this->addSql('DROP TABLE snack'); 44 | $this->addSql('CREATE TEMPORARY TABLE __temp__snack_machine AS SELECT id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM snack_machine'); 45 | $this->addSql('DROP TABLE snack_machine'); 46 | $this->addSql('CREATE TABLE snack_machine (id INTEGER NOT NULL, money_inside_one_cent_count INTEGER NOT NULL, money_inside_ten_cent_count INTEGER NOT NULL, money_inside_quarter_count INTEGER NOT NULL, money_inside_one_dollar_count INTEGER NOT NULL, money_inside_five_dollar_count INTEGER NOT NULL, money_inside_twenty_dollar_count INTEGER NOT NULL, money_in_transaction_one_cent_count INTEGER NOT NULL, money_in_transaction_ten_cent_count INTEGER NOT NULL, money_in_transaction_quarter_count INTEGER NOT NULL, money_in_transaction_one_dollar_count INTEGER NOT NULL, money_in_transaction_five_dollar_count INTEGER NOT NULL, money_in_transaction_twenty_dollar_count INTEGER NOT NULL, PRIMARY KEY(id))'); 47 | $this->addSql('INSERT INTO snack_machine (id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count) SELECT id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM __temp__snack_machine'); 48 | $this->addSql('DROP TABLE __temp__snack_machine'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/unit/SnackMachineTest.php: -------------------------------------------------------------------------------- 1 | insertMoney(Money::Cent()); 21 | $snackMachine->insertMoney(Money::Dollar()); 22 | 23 | $this->assertEquals(1.01, $snackMachine->getMoneyInTransaction()); 24 | } 25 | 26 | public function testCannotInsertMoreThanOneCoinOrNoteAtATime() 27 | { 28 | $snackMachine = new SnackMachine(); 29 | $twoCent = Money::Cent()->add(Money::Cent()); 30 | 31 | $this->expectException(LogicException::class); 32 | 33 | $snackMachine->insertMoney($twoCent); 34 | } 35 | 36 | public function test_return_money_empties_money_in_transaction() 37 | { 38 | $snackMachine = new SnackMachine(); 39 | $snackMachine->insertMoney(Money::Cent()); 40 | 41 | $snackMachine->returnMoney(); 42 | 43 | $this->assertEquals(0, $snackMachine->getMoneyInTransaction()); 44 | } 45 | 46 | public function test_buy_snack_trades_inserted_money_for_a_snack() 47 | { 48 | $snackMachine = new SnackMachine(); 49 | $snackMachine->loadSnack(1, new SnackPile(Snack::Chocolate(), 3, 1)); 50 | $snackMachine->insertMoney(Money::Dollar()); 51 | 52 | $snackMachine->buySnack(1); 53 | 54 | $this->assertEquals(0, $snackMachine->getMoneyInTransaction()); 55 | $this->assertEquals(1, $snackMachine->getMoneyInside()->getAmount()); 56 | 57 | $this->assertEquals(2, $snackMachine->getSnackPile(1)->getQuantity()); 58 | } 59 | 60 | public function test_cannot_make_purchase_when_there_is_no_snack() 61 | { 62 | $this->expectException(InvalidOperationException::class); 63 | 64 | $snackMachine = new SnackMachine(); 65 | $snackMachine->buySnack(1); 66 | } 67 | 68 | public function test_cannot_make_purchase_if_not_enough_money_inserted() 69 | { 70 | $snackMachine = new SnackMachine(); 71 | $snackMachine->loadSnack(1, new SnackPile(Snack::Chocolate(), 1, 2)); 72 | $snackMachine->insertMoney(Money::Dollar()); 73 | 74 | $this->expectException(InvalidOperationException::class); 75 | 76 | $snackMachine->buySnack(1); 77 | } 78 | 79 | public function test_snack_machine_returns_money_with_highest_denomination_first() 80 | { 81 | $snackMachine = new SnackMachine(); 82 | $snackMachine->loadMoney(Money::Dollar()); 83 | 84 | $snackMachine->insertMoney(Money::Quarter()); 85 | $snackMachine->insertMoney(Money::Quarter()); 86 | $snackMachine->insertMoney(Money::Quarter()); 87 | $snackMachine->insertMoney(Money::Quarter()); 88 | $returnedMoney = $snackMachine->returnMoney(); 89 | 90 | $this->assertEquals(4, $snackMachine->getMoneyInside()->getQuarterCount()); 91 | $this->assertEquals(0, $snackMachine->getMoneyInside()->getOneDollarCount()); 92 | $this->assertEquals(1, $returnedMoney->getOneDollarCount()); 93 | } 94 | 95 | public function test_after_purchase_change_is_returned() 96 | { 97 | $snackMachine = new SnackMachine(); 98 | $snackMachine->loadSnack(1, new SnackPile(Snack::Chocolate(), 1, 0.5)); 99 | $snackMachine->loadMoney(Money::TenCent()->multiply(10)); 100 | 101 | 102 | $snackMachine->insertMoney(Money::Dollar()); 103 | $snackMachine->buySnack(1); 104 | 105 | $this->assertEquals(1.5, $snackMachine->getMoneyInside()->getAmount()); 106 | $this->assertEquals(0, $snackMachine->getMoneyInTransaction()); 107 | } 108 | 109 | public function test_cannot_buy_snack_if_not_enough_change() 110 | { 111 | $snackMachine = new SnackMachine(); 112 | $snackMachine->loadSnack(1, new SnackPile(Snack::Chocolate(), 1, 0.5)); 113 | $snackMachine->insertMoney(Money::Dollar()); 114 | 115 | $this->expectException(InvalidOperationException::class); 116 | 117 | $snackMachine->buySnack(1); 118 | 119 | } 120 | 121 | public function test_get_all_snack_piles() 122 | { 123 | $snackMachine = new SnackMachine(); 124 | $sp1 = new SnackPile(Snack::Chocolate(), 1, 0.1); 125 | $sp2 = new SnackPile(Snack::Soda(), 2, 0.2); 126 | $sp3 = new SnackPile(Snack::Gum(), 3, 0.3); 127 | $snackMachine->loadSnack(1, $sp1); 128 | $snackMachine->loadSnack(2, $sp2); 129 | $snackMachine->loadSnack(3, $sp3); 130 | 131 | $snackPiles = $snackMachine->getAllSnackPiles(); 132 | 133 | $this->assertCount(3, $snackPiles); 134 | $this->assertEquals($sp1, $snackPiles[0]); 135 | $this->assertEquals($sp2, $snackPiles[1]); 136 | $this->assertEquals($sp3, $snackPiles[2]); 137 | 138 | // Check read only collection 139 | $snackPiles[3] = SnackPile::Empty(); 140 | $this->assertCount(3, $snackMachine->getAllSnackPiles()); 141 | } 142 | 143 | public function test_unload_money(): void 144 | { 145 | $snackMachine = new SnackMachine(); 146 | $snackMachine->loadMoney(Money::TenCent()); 147 | 148 | $money = $snackMachine->unloadMoney(); 149 | 150 | $this->assertEquals(0.1, $money->getAmount()); 151 | $this->assertEquals(0, $snackMachine->getMoneyInside()->getAmount()); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Domain/SharedKernel/Money.php: -------------------------------------------------------------------------------- 1 | oneCentCount = $oneCentCount; 40 | $this->tenCentCount = $tenCentCount; 41 | $this->quarterCount = $quarterCount; 42 | $this->oneDollarCount = $oneDollarCount; 43 | $this->fiveDollarCount = $fiveDollarCount; 44 | $this->twentyDollarCount = $twentyDollarCount; 45 | } 46 | 47 | public static function TwentyDollar(): Money 48 | { 49 | return new Money(0, 0, 0, 0, 0, 1); 50 | } 51 | 52 | public static function FiveDollar(): Money 53 | { 54 | return new Money(0, 0, 0, 0, 1, 0); 55 | } 56 | 57 | public static function Dollar(): Money 58 | { 59 | return new Money(0, 0, 0, 1, 0, 0); 60 | } 61 | 62 | public static function Quarter(): Money 63 | { 64 | return new Money(0, 0, 1, 0, 0, 0); 65 | } 66 | 67 | public static function TenCent(): Money 68 | { 69 | return new Money(0, 1, 0, 0, 0, 0); 70 | } 71 | 72 | public static function Cent(): Money 73 | { 74 | return new Money(1, 0, 0, 0, 0, 0); 75 | } 76 | 77 | public static function None(): Money 78 | { 79 | return new Money(0, 0, 0, 0, 0, 0); 80 | } 81 | 82 | public function add(Money $m): Money 83 | { 84 | return new Money( 85 | $this->oneCentCount + $m->oneCentCount, 86 | $this->tenCentCount + $m->tenCentCount, 87 | $this->quarterCount + $m->quarterCount, 88 | $this->oneDollarCount + $m->oneDollarCount, 89 | $this->fiveDollarCount + $m->fiveDollarCount, 90 | $this->twentyDollarCount + $m->twentyDollarCount 91 | ); 92 | } 93 | 94 | public function sub(Money $m): Money 95 | { 96 | return new Money( 97 | $this->oneCentCount - $m->oneCentCount, 98 | $this->tenCentCount - $m->tenCentCount, 99 | $this->quarterCount - $m->quarterCount, 100 | $this->oneDollarCount - $m->oneDollarCount, 101 | $this->fiveDollarCount - $m->fiveDollarCount, 102 | $this->twentyDollarCount - $m->twentyDollarCount 103 | ); 104 | } 105 | 106 | public function multiply(int $multiplier): Money 107 | { 108 | return new Money( 109 | $this->oneCentCount * $multiplier, 110 | $this->tenCentCount * $multiplier, 111 | $this->quarterCount * $multiplier, 112 | $this->oneDollarCount * $multiplier, 113 | $this->fiveDollarCount * $multiplier, 114 | $this->twentyDollarCount * $multiplier 115 | ); 116 | } 117 | 118 | public function getAmount() 119 | { 120 | return 121 | 0.01 * $this->oneCentCount + 122 | 0.1 * $this->tenCentCount + 123 | 0.25 * $this->quarterCount + 124 | 1 * $this->oneDollarCount + 125 | 5 * $this->fiveDollarCount + 126 | 20 * $this->twentyDollarCount; 127 | } 128 | 129 | public function getOneCentCount() 130 | { 131 | return $this->oneCentCount; 132 | } 133 | 134 | public function getTenCentCount() 135 | { 136 | return $this->tenCentCount; 137 | } 138 | 139 | public function getQuarterCount() 140 | { 141 | return $this->quarterCount; 142 | } 143 | 144 | public function getOneDollarCount() 145 | { 146 | return $this->oneDollarCount; 147 | } 148 | 149 | public function getFiveDollarCount() 150 | { 151 | return $this->fiveDollarCount; 152 | } 153 | 154 | public function getTwentyDollarCount() 155 | { 156 | return $this->twentyDollarCount; 157 | } 158 | 159 | public function isEquals($obj): bool 160 | { 161 | return 162 | $obj instanceof self && 163 | $this->oneCentCount === $obj->oneCentCount && 164 | $this->tenCentCount === $obj->tenCentCount && 165 | $this->quarterCount === $obj->quarterCount && 166 | $this->oneDollarCount === $obj->oneDollarCount && 167 | $this->fiveDollarCount === $obj->fiveDollarCount && 168 | $this->twentyDollarCount === $obj->twentyDollarCount; 169 | } 170 | 171 | public function __toString() 172 | { 173 | return Utility::moneyToString($this->getAmount()); 174 | } 175 | 176 | public function allocate(float $amount): Money 177 | { 178 | $twentyDollarCount = min((int)($amount / 20), $this->twentyDollarCount); 179 | $amount -= $twentyDollarCount * 20; 180 | 181 | $fiveDollarCount = min((int)($amount / 5), $this->fiveDollarCount); 182 | $amount -= $fiveDollarCount * 5; 183 | 184 | $oneDollarCount = min((int)$amount, $this->oneDollarCount); 185 | $amount -= $oneDollarCount; 186 | 187 | $quarterCount = min((int)($amount / 0.25), $this->quarterCount); 188 | $amount -= $quarterCount * 0.25; 189 | 190 | $tenCentCount = min((int)($amount / 0.1), $this->tenCentCount); 191 | $amount -= $tenCentCount * 0.1; 192 | 193 | $oneCentCount = min($amount / 0.01, $this->oneCentCount); 194 | 195 | return new Money( 196 | $oneCentCount, 197 | $tenCentCount, 198 | $quarterCount, 199 | $oneDollarCount, 200 | $fiveDollarCount, 201 | $twentyDollarCount); 202 | } 203 | } -------------------------------------------------------------------------------- /src/Infrastructure/Migrations/Version20180607093723.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); 17 | 18 | $this->addSql('CREATE TABLE atm (id INTEGER NOT NULL, money_charged DOUBLE PRECISION NOT NULL, money_inside_one_cent_count INTEGER NOT NULL, money_inside_ten_cent_count INTEGER NOT NULL, money_inside_quarter_count INTEGER NOT NULL, money_inside_one_dollar_count INTEGER NOT NULL, money_inside_five_dollar_count INTEGER NOT NULL, money_inside_twenty_dollar_count INTEGER NOT NULL, PRIMARY KEY(id))'); 19 | $this->addSql('DROP INDEX IDX_AC0E2067B0081B53'); 20 | $this->addSql('CREATE TEMPORARY TABLE __temp__slot AS SELECT id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price FROM slot'); 21 | $this->addSql('DROP TABLE slot'); 22 | $this->addSql('CREATE TABLE slot (id INTEGER NOT NULL, snack_machine_id INTEGER DEFAULT NULL, position INTEGER NOT NULL, snack_pile_snack_id INTEGER NOT NULL, snack_pile_quantity INTEGER NOT NULL, snack_pile_price DOUBLE PRECISION NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_AC0E2067B0081B53 FOREIGN KEY (snack_machine_id) REFERENCES snack_machine (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); 23 | $this->addSql('INSERT INTO slot (id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price) SELECT id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price FROM __temp__slot'); 24 | $this->addSql('DROP TABLE __temp__slot'); 25 | $this->addSql('CREATE INDEX IDX_AC0E2067B0081B53 ON slot (snack_machine_id)'); 26 | $this->addSql('CREATE TEMPORARY TABLE __temp__snack AS SELECT id, name FROM snack'); 27 | $this->addSql('DROP TABLE snack'); 28 | $this->addSql('CREATE TABLE snack (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, PRIMARY KEY(id))'); 29 | $this->addSql('INSERT INTO snack (id, name) SELECT id, name FROM __temp__snack'); 30 | $this->addSql('DROP TABLE __temp__snack'); 31 | $this->addSql('CREATE TEMPORARY TABLE __temp__snack_machine AS SELECT id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count, money_in_transaction FROM snack_machine'); 32 | $this->addSql('DROP TABLE snack_machine'); 33 | $this->addSql('CREATE TABLE snack_machine (id INTEGER NOT NULL, money_inside_one_cent_count INTEGER NOT NULL, money_inside_ten_cent_count INTEGER NOT NULL, money_inside_quarter_count INTEGER NOT NULL, money_inside_one_dollar_count INTEGER NOT NULL, money_inside_five_dollar_count INTEGER NOT NULL, money_inside_twenty_dollar_count INTEGER NOT NULL, money_in_transaction DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); 34 | $this->addSql('INSERT INTO snack_machine (id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count, money_in_transaction) SELECT id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count, money_in_transaction FROM __temp__snack_machine'); 35 | $this->addSql('DROP TABLE __temp__snack_machine'); 36 | 37 | $this->addSql('INSERT INTO atm(id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count, money_charged) 38 | VALUES (1,100,100,100,100,100,100,0)'); 39 | } 40 | 41 | public function down(Schema $schema) 42 | { 43 | // this down() migration is auto-generated, please modify it to your needs 44 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); 45 | 46 | $this->addSql('DROP TABLE atm'); 47 | $this->addSql('DROP INDEX IDX_AC0E2067B0081B53'); 48 | $this->addSql('CREATE TEMPORARY TABLE __temp__slot AS SELECT id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price FROM slot'); 49 | $this->addSql('DROP TABLE slot'); 50 | $this->addSql('CREATE TABLE slot (id INTEGER NOT NULL, snack_machine_id INTEGER DEFAULT NULL, position INTEGER NOT NULL, snack_pile_snack_id INTEGER NOT NULL, snack_pile_quantity INTEGER NOT NULL, snack_pile_price DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); 51 | $this->addSql('INSERT INTO slot (id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price) SELECT id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price FROM __temp__slot'); 52 | $this->addSql('DROP TABLE __temp__slot'); 53 | $this->addSql('CREATE INDEX IDX_AC0E2067B0081B53 ON slot (snack_machine_id)'); 54 | $this->addSql('CREATE TEMPORARY TABLE __temp__snack AS SELECT id, name FROM snack'); 55 | $this->addSql('DROP TABLE snack'); 56 | $this->addSql('CREATE TABLE snack (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); 57 | $this->addSql('INSERT INTO snack (id, name) SELECT id, name FROM __temp__snack'); 58 | $this->addSql('DROP TABLE __temp__snack'); 59 | $this->addSql('CREATE TEMPORARY TABLE __temp__snack_machine AS SELECT id, money_in_transaction, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM snack_machine'); 60 | $this->addSql('DROP TABLE snack_machine'); 61 | $this->addSql('CREATE TABLE snack_machine (id INTEGER NOT NULL, money_in_transaction DOUBLE PRECISION NOT NULL, money_inside_one_cent_count INTEGER NOT NULL, money_inside_ten_cent_count INTEGER NOT NULL, money_inside_quarter_count INTEGER NOT NULL, money_inside_one_dollar_count INTEGER NOT NULL, money_inside_five_dollar_count INTEGER NOT NULL, money_inside_twenty_dollar_count INTEGER NOT NULL, PRIMARY KEY(id))'); 62 | $this->addSql('INSERT INTO snack_machine (id, money_in_transaction, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count) SELECT id, money_in_transaction, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM __temp__snack_machine'); 63 | $this->addSql('DROP TABLE __temp__snack_machine'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "doctrine/annotations": { 3 | "version": "1.0", 4 | "recipe": { 5 | "repo": "github.com/symfony/recipes", 6 | "branch": "master", 7 | "version": "1.0", 8 | "ref": "cb4152ebcadbe620ea2261da1a1c5a9b8cea7672" 9 | } 10 | }, 11 | "doctrine/cache": { 12 | "version": "v1.7.1" 13 | }, 14 | "doctrine/collections": { 15 | "version": "v1.5.0" 16 | }, 17 | "doctrine/common": { 18 | "version": "v2.8.1" 19 | }, 20 | "doctrine/dbal": { 21 | "version": "v2.7.1" 22 | }, 23 | "doctrine/doctrine-bundle": { 24 | "version": "1.6", 25 | "recipe": { 26 | "repo": "github.com/symfony/recipes", 27 | "branch": "master", 28 | "version": "1.6", 29 | "ref": "ae205d5114e719deb64d2110f56ef910787d1e04" 30 | } 31 | }, 32 | "doctrine/doctrine-cache-bundle": { 33 | "version": "1.3.3" 34 | }, 35 | "doctrine/doctrine-migrations-bundle": { 36 | "version": "1.2", 37 | "recipe": { 38 | "repo": "github.com/symfony/recipes", 39 | "branch": "master", 40 | "version": "1.2", 41 | "ref": "c1431086fec31f17fbcfe6d6d7e92059458facc1" 42 | } 43 | }, 44 | "doctrine/event-manager": { 45 | "version": "v1.0.0" 46 | }, 47 | "doctrine/inflector": { 48 | "version": "v1.3.0" 49 | }, 50 | "doctrine/instantiator": { 51 | "version": "1.1.0" 52 | }, 53 | "doctrine/lexer": { 54 | "version": "v1.0.1" 55 | }, 56 | "doctrine/migrations": { 57 | "version": "v1.6.2" 58 | }, 59 | "doctrine/orm": { 60 | "version": "v2.6.1" 61 | }, 62 | "doctrine/persistence": { 63 | "version": "v1.0.1" 64 | }, 65 | "doctrine/reflection": { 66 | "version": "v1.0.0" 67 | }, 68 | "jdorn/sql-formatter": { 69 | "version": "v1.2.17" 70 | }, 71 | "myclabs/deep-copy": { 72 | "version": "1.7.0" 73 | }, 74 | "nikic/php-parser": { 75 | "version": "v4.0.1" 76 | }, 77 | "ocramius/package-versions": { 78 | "version": "1.3.0" 79 | }, 80 | "ocramius/proxy-manager": { 81 | "version": "2.1.1" 82 | }, 83 | "phar-io/manifest": { 84 | "version": "1.0.1" 85 | }, 86 | "phar-io/version": { 87 | "version": "1.0.1" 88 | }, 89 | "phpdocumentor/reflection-common": { 90 | "version": "1.0.1" 91 | }, 92 | "phpdocumentor/reflection-docblock": { 93 | "version": "4.3.0" 94 | }, 95 | "phpdocumentor/type-resolver": { 96 | "version": "0.4.0" 97 | }, 98 | "phpspec/prophecy": { 99 | "version": "1.7.6" 100 | }, 101 | "phpunit/php-code-coverage": { 102 | "version": "6.0.3" 103 | }, 104 | "phpunit/php-file-iterator": { 105 | "version": "1.4.5" 106 | }, 107 | "phpunit/php-text-template": { 108 | "version": "1.2.1" 109 | }, 110 | "phpunit/php-timer": { 111 | "version": "2.0.0" 112 | }, 113 | "phpunit/php-token-stream": { 114 | "version": "3.0.0" 115 | }, 116 | "phpunit/phpunit": { 117 | "version": "4.7", 118 | "recipe": { 119 | "repo": "github.com/symfony/recipes", 120 | "branch": "master", 121 | "version": "4.7", 122 | "ref": "c276fa48d4713de91eb410289b3b1834acb7e403" 123 | } 124 | }, 125 | "phpunit/phpunit-mock-objects": { 126 | "version": "6.1.1" 127 | }, 128 | "psr/cache": { 129 | "version": "1.0.1" 130 | }, 131 | "psr/container": { 132 | "version": "1.0.0" 133 | }, 134 | "psr/log": { 135 | "version": "1.0.2" 136 | }, 137 | "psr/simple-cache": { 138 | "version": "1.0.1" 139 | }, 140 | "sebastian/code-unit-reverse-lookup": { 141 | "version": "1.0.1" 142 | }, 143 | "sebastian/comparator": { 144 | "version": "3.0.0" 145 | }, 146 | "sebastian/diff": { 147 | "version": "3.0.0" 148 | }, 149 | "sebastian/environment": { 150 | "version": "3.1.0" 151 | }, 152 | "sebastian/exporter": { 153 | "version": "3.1.0" 154 | }, 155 | "sebastian/global-state": { 156 | "version": "2.0.0" 157 | }, 158 | "sebastian/object-enumerator": { 159 | "version": "3.0.3" 160 | }, 161 | "sebastian/object-reflector": { 162 | "version": "1.1.1" 163 | }, 164 | "sebastian/recursion-context": { 165 | "version": "3.0.0" 166 | }, 167 | "sebastian/resource-operations": { 168 | "version": "1.0.0" 169 | }, 170 | "sebastian/version": { 171 | "version": "2.0.1" 172 | }, 173 | "sensio/framework-extra-bundle": { 174 | "version": "4.0", 175 | "recipe": { 176 | "repo": "github.com/symfony/recipes", 177 | "branch": "master", 178 | "version": "4.0", 179 | "ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543" 180 | } 181 | }, 182 | "symfony/cache": { 183 | "version": "v4.0.6" 184 | }, 185 | "symfony/config": { 186 | "version": "v4.0.6" 187 | }, 188 | "symfony/console": { 189 | "version": "3.3", 190 | "recipe": { 191 | "repo": "github.com/symfony/recipes", 192 | "branch": "master", 193 | "version": "3.3", 194 | "ref": "c646e4b71af082e94b5014daca36ef6812bad076" 195 | } 196 | }, 197 | "symfony/debug": { 198 | "version": "v4.0.6" 199 | }, 200 | "symfony/dependency-injection": { 201 | "version": "v4.0.6" 202 | }, 203 | "symfony/doctrine-bridge": { 204 | "version": "v4.0.9" 205 | }, 206 | "symfony/dotenv": { 207 | "version": "v4.0.6" 208 | }, 209 | "symfony/event-dispatcher": { 210 | "version": "v4.0.6" 211 | }, 212 | "symfony/filesystem": { 213 | "version": "v4.0.6" 214 | }, 215 | "symfony/finder": { 216 | "version": "v4.0.6" 217 | }, 218 | "symfony/flex": { 219 | "version": "1.0", 220 | "recipe": { 221 | "repo": "github.com/symfony/recipes", 222 | "branch": "master", 223 | "version": "1.0", 224 | "ref": "cc1afd81841db36fbef982fe56b48ade6716fac4" 225 | } 226 | }, 227 | "symfony/framework-bundle": { 228 | "version": "3.3", 229 | "recipe": { 230 | "repo": "github.com/symfony/recipes", 231 | "branch": "master", 232 | "version": "3.3", 233 | "ref": "b9f462a47f7fd28d56c61f59c027fd7ad8e1aac8" 234 | } 235 | }, 236 | "symfony/http-foundation": { 237 | "version": "v4.0.6" 238 | }, 239 | "symfony/http-kernel": { 240 | "version": "v4.0.6" 241 | }, 242 | "symfony/maker-bundle": { 243 | "version": "1.0", 244 | "recipe": { 245 | "repo": "github.com/symfony/recipes", 246 | "branch": "master", 247 | "version": "1.0", 248 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" 249 | } 250 | }, 251 | "symfony/orm-pack": { 252 | "version": "v1.0.5" 253 | }, 254 | "symfony/polyfill-ctype": { 255 | "version": "v1.9.0" 256 | }, 257 | "symfony/polyfill-mbstring": { 258 | "version": "v1.7.0" 259 | }, 260 | "symfony/proxy-manager-bridge": { 261 | "version": "v4.1.4" 262 | }, 263 | "symfony/routing": { 264 | "version": "4.0", 265 | "recipe": { 266 | "repo": "github.com/symfony/recipes", 267 | "branch": "master", 268 | "version": "4.0", 269 | "ref": "cda8b550123383d25827705d05a42acf6819fe4e" 270 | } 271 | }, 272 | "symfony/twig-bridge": { 273 | "version": "v4.0.6" 274 | }, 275 | "symfony/twig-bundle": { 276 | "version": "3.3", 277 | "recipe": { 278 | "repo": "github.com/symfony/recipes", 279 | "branch": "master", 280 | "version": "3.3", 281 | "ref": "f75ac166398e107796ca94cc57fa1edaa06ec47f" 282 | } 283 | }, 284 | "symfony/yaml": { 285 | "version": "v4.0.6" 286 | }, 287 | "theseer/tokenizer": { 288 | "version": "1.1.0" 289 | }, 290 | "twig/twig": { 291 | "version": "v2.4.6" 292 | }, 293 | "webmozart/assert": { 294 | "version": "1.3.0" 295 | }, 296 | "zendframework/zend-code": { 297 | "version": "3.3.0" 298 | }, 299 | "zendframework/zend-eventmanager": { 300 | "version": "3.2.1" 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/Infrastructure/Migrations/Version20180607235635.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); 17 | 18 | $this->addSql('CREATE TABLE head_office (id INTEGER NOT NULL, balance DOUBLE PRECISION NOT NULL, cash_one_cent_count INTEGER NOT NULL, cash_ten_cent_count INTEGER NOT NULL, cash_quarter_count INTEGER NOT NULL, cash_one_dollar_count INTEGER NOT NULL, cash_five_dollar_count INTEGER NOT NULL, cash_twenty_dollar_count INTEGER NOT NULL, PRIMARY KEY(id))'); 19 | $this->addSql('CREATE TEMPORARY TABLE __temp__atm AS SELECT id, money_charged, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM atm'); 20 | $this->addSql('DROP TABLE atm'); 21 | $this->addSql('CREATE TABLE atm (id INTEGER NOT NULL, money_charged DOUBLE PRECISION NOT NULL, money_inside_one_cent_count INTEGER NOT NULL, money_inside_ten_cent_count INTEGER NOT NULL, money_inside_quarter_count INTEGER NOT NULL, money_inside_one_dollar_count INTEGER NOT NULL, money_inside_five_dollar_count INTEGER NOT NULL, money_inside_twenty_dollar_count INTEGER NOT NULL, PRIMARY KEY(id))'); 22 | $this->addSql('INSERT INTO atm (id, money_charged, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count) SELECT id, money_charged, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM __temp__atm'); 23 | $this->addSql('DROP TABLE __temp__atm'); 24 | $this->addSql('DROP INDEX IDX_AC0E2067B0081B53'); 25 | $this->addSql('CREATE TEMPORARY TABLE __temp__slot AS SELECT id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price FROM slot'); 26 | $this->addSql('DROP TABLE slot'); 27 | $this->addSql('CREATE TABLE slot (id INTEGER NOT NULL, snack_machine_id INTEGER DEFAULT NULL, position INTEGER NOT NULL, snack_pile_snack_id INTEGER NOT NULL, snack_pile_quantity INTEGER NOT NULL, snack_pile_price DOUBLE PRECISION NOT NULL, PRIMARY KEY(id), CONSTRAINT FK_AC0E2067B0081B53 FOREIGN KEY (snack_machine_id) REFERENCES snack_machine (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); 28 | $this->addSql('INSERT INTO slot (id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price) SELECT id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price FROM __temp__slot'); 29 | $this->addSql('DROP TABLE __temp__slot'); 30 | $this->addSql('CREATE INDEX IDX_AC0E2067B0081B53 ON slot (snack_machine_id)'); 31 | $this->addSql('CREATE TEMPORARY TABLE __temp__snack AS SELECT id, name FROM snack'); 32 | $this->addSql('DROP TABLE snack'); 33 | $this->addSql('CREATE TABLE snack (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL COLLATE BINARY, PRIMARY KEY(id))'); 34 | $this->addSql('INSERT INTO snack (id, name) SELECT id, name FROM __temp__snack'); 35 | $this->addSql('DROP TABLE __temp__snack'); 36 | $this->addSql('CREATE TEMPORARY TABLE __temp__snack_machine AS SELECT id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count, money_in_transaction FROM snack_machine'); 37 | $this->addSql('DROP TABLE snack_machine'); 38 | $this->addSql('CREATE TABLE snack_machine (id INTEGER NOT NULL, money_inside_one_cent_count INTEGER NOT NULL, money_inside_ten_cent_count INTEGER NOT NULL, money_inside_quarter_count INTEGER NOT NULL, money_inside_one_dollar_count INTEGER NOT NULL, money_inside_five_dollar_count INTEGER NOT NULL, money_inside_twenty_dollar_count INTEGER NOT NULL, money_in_transaction DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); 39 | $this->addSql('INSERT INTO snack_machine (id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count, money_in_transaction) SELECT id, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count, money_in_transaction FROM __temp__snack_machine'); 40 | $this->addSql('DROP TABLE __temp__snack_machine'); 41 | 42 | 43 | $this->addSql('INSERT INTO head_office(id, cash_one_cent_count, cash_ten_cent_count, cash_quarter_count, cash_one_dollar_count, cash_five_dollar_count, cash_twenty_dollar_count, balance) 44 | VALUES (1,20,20,20,20,20,20,10)'); 45 | } 46 | 47 | public function down(Schema $schema) 48 | { 49 | // this down() migration is auto-generated, please modify it to your needs 50 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'sqlite', 'Migration can only be executed safely on \'sqlite\'.'); 51 | 52 | $this->addSql('DROP TABLE head_office'); 53 | $this->addSql('CREATE TEMPORARY TABLE __temp__atm AS SELECT id, money_charged, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM atm'); 54 | $this->addSql('DROP TABLE atm'); 55 | $this->addSql('CREATE TABLE atm (id INTEGER NOT NULL, money_charged DOUBLE PRECISION NOT NULL, money_inside_one_cent_count INTEGER NOT NULL, money_inside_ten_cent_count INTEGER NOT NULL, money_inside_quarter_count INTEGER NOT NULL, money_inside_one_dollar_count INTEGER NOT NULL, money_inside_five_dollar_count INTEGER NOT NULL, money_inside_twenty_dollar_count INTEGER NOT NULL, PRIMARY KEY(id))'); 56 | $this->addSql('INSERT INTO atm (id, money_charged, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count) SELECT id, money_charged, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM __temp__atm'); 57 | $this->addSql('DROP TABLE __temp__atm'); 58 | $this->addSql('DROP INDEX IDX_AC0E2067B0081B53'); 59 | $this->addSql('CREATE TEMPORARY TABLE __temp__slot AS SELECT id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price FROM slot'); 60 | $this->addSql('DROP TABLE slot'); 61 | $this->addSql('CREATE TABLE slot (id INTEGER NOT NULL, snack_machine_id INTEGER DEFAULT NULL, position INTEGER NOT NULL, snack_pile_snack_id INTEGER NOT NULL, snack_pile_quantity INTEGER NOT NULL, snack_pile_price DOUBLE PRECISION NOT NULL, PRIMARY KEY(id))'); 62 | $this->addSql('INSERT INTO slot (id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price) SELECT id, snack_machine_id, position, snack_pile_snack_id, snack_pile_quantity, snack_pile_price FROM __temp__slot'); 63 | $this->addSql('DROP TABLE __temp__slot'); 64 | $this->addSql('CREATE INDEX IDX_AC0E2067B0081B53 ON slot (snack_machine_id)'); 65 | $this->addSql('CREATE TEMPORARY TABLE __temp__snack AS SELECT id, name FROM snack'); 66 | $this->addSql('DROP TABLE snack'); 67 | $this->addSql('CREATE TABLE snack (id INTEGER NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); 68 | $this->addSql('INSERT INTO snack (id, name) SELECT id, name FROM __temp__snack'); 69 | $this->addSql('DROP TABLE __temp__snack'); 70 | $this->addSql('CREATE TEMPORARY TABLE __temp__snack_machine AS SELECT id, money_in_transaction, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM snack_machine'); 71 | $this->addSql('DROP TABLE snack_machine'); 72 | $this->addSql('CREATE TABLE snack_machine (id INTEGER NOT NULL, money_in_transaction DOUBLE PRECISION NOT NULL, money_inside_one_cent_count INTEGER NOT NULL, money_inside_ten_cent_count INTEGER NOT NULL, money_inside_quarter_count INTEGER NOT NULL, money_inside_one_dollar_count INTEGER NOT NULL, money_inside_five_dollar_count INTEGER NOT NULL, money_inside_twenty_dollar_count INTEGER NOT NULL, PRIMARY KEY(id))'); 73 | $this->addSql('INSERT INTO snack_machine (id, money_in_transaction, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count) SELECT id, money_in_transaction, money_inside_one_cent_count, money_inside_ten_cent_count, money_inside_quarter_count, money_inside_one_dollar_count, money_inside_five_dollar_count, money_inside_twenty_dollar_count FROM __temp__snack_machine'); 74 | $this->addSql('DROP TABLE __temp__snack_machine'); 75 | } 76 | } 77 | --------------------------------------------------------------------------------