├── var ├── cache │ └── .gitkeep ├── logs │ └── .gitkeep └── sessions │ └── .gitkeep ├── tests ├── _data │ └── .gitkeep ├── unit │ └── notes.md ├── functional │ └── notes.md ├── _output │ └── .gitignore ├── _support │ ├── _generated │ │ └── .gitignore │ ├── Helper │ │ ├── Api.php │ │ ├── Unit.php │ │ ├── Acceptance.php │ │ └── Functional.php │ ├── UnitTester.php │ ├── AcceptanceTester.php │ └── FunctionalTester.php ├── unit.suite.yml ├── api.suite.yml ├── api │ ├── CreateRideCest.php │ ├── CreatePassengerCest.php │ ├── MarkRideAcceptedByDriverCest.php │ ├── MarkRideInProgressCest.php │ ├── MarkRideCompletedCest.php │ └── AuthenticationCest.php ├── acceptance.suite.yml ├── functional.suite.yml ├── AppBundle │ ├── Entity │ │ ├── RideEventTypeTest.php │ │ └── AppRoleTest.php │ ├── Location │ │ ├── LocationServiceTest.php │ │ └── LocationRepositoryTest.php │ ├── User │ │ ├── FakeUser.php │ │ ├── UserServiceTest.php │ │ ├── FakeUserManager.php │ │ └── UserRepositoryTest.php │ ├── DTO │ │ ├── UserDtoTest.php │ │ └── RideDtoTest.php │ ├── Production │ │ ├── LocationApi.php │ │ └── UserApi.php │ ├── Ride │ │ ├── RideRepositoryTest.php │ │ ├── RideTransitionTest.php │ │ └── RideEventRepositoryTest.php │ └── AppTestCase.php └── acceptance │ └── FirstCest.php ├── web ├── favicon.ico ├── apple-touch-icon.png ├── robots.txt ├── app.php ├── app_dev.php └── .htaccess ├── app ├── config │ ├── routing_test.yml │ ├── routing_dev.yml │ ├── services.yml │ ├── config_prod.yml │ ├── parameters.yml.dist │ ├── routing.yml │ ├── config_dev.yml │ ├── config_test.yml │ ├── config.yml │ ├── security.yml │ └── frameworks.yml ├── AppCache.php ├── .htaccess ├── autoload.php ├── Resources │ └── views │ │ ├── base.html.twig │ │ └── default │ │ └── index.html.twig ├── DoctrineMigrations │ ├── Version20180330191116.php │ ├── Version20180208222831.php │ ├── Version20180212012515.php │ ├── Version20180212012516.php │ ├── Version20180131035822.php │ ├── Version20181027033505.php │ ├── Version20180131035823.php │ ├── Version20180208222514.php │ ├── Version20180211230225.php │ └── Version20180131035821.php └── AppKernel.php ├── .idea ├── encodings.xml ├── misc.xml ├── vcs.xml ├── scopes │ ├── web_scope.xml │ └── psr2_scope.xml ├── modules.xml ├── symfony2.xml ├── runConfigurations │ ├── API.xml │ └── Tests.xml ├── php-test-framework.xml ├── php.xml └── kata_tdd_php_symfony.iml ├── src ├── .htaccess └── AppBundle │ ├── AppBundle.php │ ├── Exception │ ├── DuplicateRoleAssignmentException.php │ ├── RideLifeCycleException.php │ ├── RideNotFoundException.php │ ├── UserNotFoundException.php │ ├── UnauthorizedOperationException.php │ ├── UserNotInDriverRoleException.php │ ├── UserNotInPassengerRoleException.php │ └── ActingDriverIsNotAssignedDriverException.php │ ├── Repository │ ├── LocationRepositoryInterface.php │ ├── AppRepository.php │ ├── RideRepositoryInterface.php │ ├── RideEventRepositoryInterface.php │ ├── UserRepositoryInterface.php │ ├── RideRepository.php │ ├── LocationRepository.php │ ├── RideEventRepository.php │ └── UserRepository.php │ ├── Entity │ ├── Client.php │ ├── AuthCode.php │ ├── AccessToken.php │ ├── RefreshToken.php │ ├── AppRole.php │ ├── RideEvent.php │ ├── RideEventType.php │ ├── AppLocation.php │ ├── Ride.php │ └── AppUser.php │ ├── Controller │ ├── DefaultController.php │ ├── UserController.php │ ├── AppController.php │ └── RideController.php │ ├── Service │ ├── LocationService.php │ ├── UserService.php │ ├── RideTransitionService.php │ └── RideService.php │ └── DTO │ ├── RideDto.php │ └── UserDto.php ├── codeception.yml ├── .gitignore ├── bin ├── console └── symfony_requirements ├── phpunit.xml ├── Kata-Tasks.md ├── template-phpunit.xml ├── README.md └── composer.json /var/cache/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var/logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/_data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/notes.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var/sessions/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/functional/notes.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/_output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/_support/_generated/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elchris/kata_tdd_php_symfony/HEAD/web/favicon.ico -------------------------------------------------------------------------------- /app/config/routing_test.yml: -------------------------------------------------------------------------------- 1 | app: 2 | resource: "@AppBundle/Controller/" 3 | type: annotation -------------------------------------------------------------------------------- /web/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elchris/kata_tdd_php_symfony/HEAD/web/apple-touch-icon.png -------------------------------------------------------------------------------- /app/AppCache.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /src/AppBundle/AppBundle.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for unit or integration tests. 4 | 5 | actor: UnitTester 6 | modules: 7 | enabled: 8 | - Asserts 9 | - \Helper\Unit -------------------------------------------------------------------------------- /.idea/scopes/web_scope.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | tests: tests 3 | output: tests/_output 4 | data: tests/_data 5 | support: tests/_support 6 | envs: tests/_envs 7 | actor_suffix: Tester 8 | extensions: 9 | enabled: 10 | - Codeception\Extension\RunFailed 11 | -------------------------------------------------------------------------------- /tests/_support/Helper/Api.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/autoload.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/symfony2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | -------------------------------------------------------------------------------- /tests/api/CreateRideCest.php: -------------------------------------------------------------------------------- 1 | getNewRide(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AppBundle/Exception/DuplicateRoleAssignmentException.php: -------------------------------------------------------------------------------- 1 | getNewPassenger(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/acceptance.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for acceptance tests. 4 | # Perform tests in browser using the WebDriver or PhpBrowser. 5 | # If you need both WebDriver and PHPBrowser tests - create a separate suite. 6 | 7 | actor: AcceptanceTester 8 | modules: 9 | enabled: 10 | - PhpBrowser: 11 | url: http://127.0.0.1:8000 12 | - \Helper\Acceptance -------------------------------------------------------------------------------- /app/config/routing_dev.yml: -------------------------------------------------------------------------------- 1 | _wdt: 2 | resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" 3 | prefix: /_wdt 4 | 5 | _profiler: 6 | resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" 7 | prefix: /_profiler 8 | 9 | _errors: 10 | resource: "@TwigBundle/Resources/config/routing/errors.xml" 11 | prefix: /_error 12 | 13 | _main: 14 | resource: routing.yml 15 | -------------------------------------------------------------------------------- /tests/functional.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | # 3 | # Suite for functional tests 4 | # Emulate web requests and make application process them 5 | # Include one of framework modules (Symfony2, Yii2, Laravel5) to use it 6 | # Remove this suite if you don't use frameworks 7 | 8 | actor: FunctionalTester 9 | modules: 10 | enabled: 11 | # add a framework module here 12 | - \Helper\Functional -------------------------------------------------------------------------------- /.idea/runConfigurations/API.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/AppBundle/Exception/RideLifeCycleException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/AppBundle/Exception/UserNotFoundException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | {% block stylesheets %}{% endblock %} 7 | 8 | 9 | 10 | {% block body %}{% endblock %} 11 | {% block javascripts %}{% endblock %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/AppBundle/Exception/UnauthorizedOperationException.php: -------------------------------------------------------------------------------- 1 | equals( 15 | RideEventType::newById(RideEventType::REQUESTED_ID) 16 | ) 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/acceptance/FirstCest.php: -------------------------------------------------------------------------------- 1 | amOnPage("/"); 21 | $I->see('Welcome'); 22 | $I->see('Symfony 3.4.37'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/AppBundle/Exception/ActingDriverIsNotAssignedDriverException.php: -------------------------------------------------------------------------------- 1 | em = $em; 21 | } 22 | 23 | public function save($object): void 24 | { 25 | $this->em->persist($object); 26 | $this->em->flush(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/api/MarkRideAcceptedByDriverCest.php: -------------------------------------------------------------------------------- 1 | getNewRide(); 15 | $driver = $I->getNewDriver(); 16 | 17 | $driverId = $driver['id']; 18 | $rideId = $requestedRide['id']; 19 | $I->acceptRideByDriver( 20 | $rideId, 21 | $driverId 22 | ); 23 | $I->assignWorkDestinationToRide($rideId); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web/app.php: -------------------------------------------------------------------------------- 1 | loadClassCache(); 11 | //$kernel = new AppCache($kernel); 12 | 13 | // When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter 14 | //Request::enableHttpMethodParameterOverride(); 15 | $request = Request::createFromGlobals(); 16 | $response = $kernel->handle($request); 17 | $response->send(); 18 | $kernel->terminate($request, $response); 19 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/AuthCode.php: -------------------------------------------------------------------------------- 1 | getNewRide(); 15 | $driver = $I->getNewDriver(); 16 | 17 | $driverId = $driver['id']; 18 | $rideId = $requestedRide['id']; 19 | 20 | $I->acceptRideByDriver( 21 | $rideId, 22 | $driverId 23 | ); 24 | $I->assignWorkDestinationToRide($rideId); 25 | $I->markRideInProgress($rideId, $driverId); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/AppBundle/Repository/RideEventRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | location()->getLocation( 19 | $referenceLocation->getLat(), 20 | $referenceLocation->getLong() 21 | ); 22 | 23 | self::assertTrue($retrievedLocation->isSameAs($referenceLocation)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/api/MarkRideCompletedCest.php: -------------------------------------------------------------------------------- 1 | getNewRide(); 15 | $driver = $I->getNewDriver(); 16 | 17 | $driverId = $driver['id']; 18 | $rideId = $requestedRide['id']; 19 | 20 | $I->acceptRideByDriver( 21 | $rideId, 22 | $driverId 23 | ); 24 | $I->assignWorkDestinationToRide($rideId); 25 | $I->markRideInProgress($rideId, $driverId); 26 | $I->markRideCompleted($rideId, $driverId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/DefaultController.php: -------------------------------------------------------------------------------- 1 | render('default/index.html.twig', [ 21 | 'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..').DIRECTORY_SEPARATOR, 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/config/parameters.yml.dist: -------------------------------------------------------------------------------- 1 | # This file is a "template" of what your parameters.yml file should look like 2 | # Set parameters here that may be different on each deployment target of the app, e.g. development, staging, production. 3 | # http://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration 4 | parameters: 5 | database_host: 127.0.0.1 6 | database_port: ~ 7 | database_name: symfony 8 | database_user: root 9 | database_password: ~ 10 | # You should uncomment this if you want use pdo_sqlite 11 | # database_path: "%kernel.root_dir%/data.db3" 12 | 13 | mailer_transport: smtp 14 | mailer_host: 127.0.0.1 15 | mailer_user: ~ 16 | mailer_password: ~ 17 | 18 | # A secret key that's used to generate certain security-related tokens 19 | secret: ThisTokenIsNotSoSecretChangeIt 20 | -------------------------------------------------------------------------------- /tests/api/AuthenticationCest.php: -------------------------------------------------------------------------------- 1 | getRegisteredUserWithToken( 17 | 'Joe', 18 | 'Passenger', 19 | true 20 | ); 21 | $userName = $newUser['user']['username']; 22 | $userId = $newUser['user']['id']; 23 | $I->nukeToken(); 24 | $response = $I->sendGetApiRequest('/user/' . $userId); 25 | $I->seeResponseContainsJson([ 26 | 'error' => 'access_denied', 27 | 'error_description' => 'OAuth2 authentication required' 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AppBundle/Repository/UserRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev'); 20 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(['--no-debug', '']) && $env !== 'prod'; 21 | 22 | if ($debug) { 23 | Debug::enable(); 24 | } 25 | 26 | $kernel = new AppKernel($env, $debug); 27 | $application = new Application($kernel); 28 | $application->run($input); 29 | -------------------------------------------------------------------------------- /tests/AppBundle/User/FakeUser.php: -------------------------------------------------------------------------------- 1 | first = $first; 24 | $this->last = $last; 25 | $baseUsername = $first . $last; 26 | $this->username = $baseUsername .microtime(true); 27 | $this->email = $this->username.'@'.$first.$last.'.com'; 28 | $this->password = 'password'; 29 | } 30 | 31 | public function toEntity() 32 | { 33 | return new AppUser( 34 | $this->first, 35 | $this->last, 36 | $this->email, 37 | $this->username, 38 | $this->password 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/AppBundle/Service/LocationService.php: -------------------------------------------------------------------------------- 1 | locationRepository = $locationRepository; 22 | } 23 | 24 | /** 25 | * @param $lat 26 | * @param $long 27 | * @return AppLocation 28 | * @throws \Exception 29 | */ 30 | public function getLocation($lat, $long): AppLocation 31 | { 32 | return $this->locationRepository->getLocation( 33 | new AppLocation( 34 | $lat, 35 | $long 36 | ) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/AppBundle/DTO/RideDto.php: -------------------------------------------------------------------------------- 1 | id = $ride->getId()->toString(); 31 | $this->passengerId = $passenger->getId()->toString(); 32 | if ($ride->hasDriver()) { 33 | $this->driverId = $driver->getId()->toString(); 34 | } 35 | if ($ride->hasDestination()) { 36 | $this->destination = $destination; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/config/config_dev.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: config.yml } 3 | - { resource: frameworks.yml } 4 | 5 | framework: 6 | router: 7 | resource: "%kernel.root_dir%/config/routing_dev.yml" 8 | strict_requirements: true 9 | profiler: { only_exceptions: false } 10 | 11 | web_profiler: 12 | toolbar: true 13 | intercept_redirects: false 14 | 15 | monolog: 16 | handlers: 17 | main: 18 | type: stream 19 | path: "%kernel.logs_dir%/%kernel.environment%.log" 20 | level: debug 21 | channels: ["!event"] 22 | console: 23 | type: console 24 | channels: ["!event", "!doctrine"] 25 | # uncomment to get logging in your browser 26 | # you may have to allow bigger header sizes in your Web server configuration 27 | #firephp: 28 | # type: firephp 29 | # level: info 30 | #chromephp: 31 | # type: chromephp 32 | # level: info 33 | 34 | #swiftmailer: 35 | # delivery_address: me@example.com 36 | -------------------------------------------------------------------------------- /.idea/php-test-framework.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/AppBundle/DTO/UserDto.php: -------------------------------------------------------------------------------- 1 | id = $id; 37 | $this->isDriver = $isDriver; 38 | $this->isPassenger = $isPassenger; 39 | $this->fullName = $fullName; 40 | $this->username = $username; 41 | $this->email = $email; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/DoctrineMigrations/Version20180330191116.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('CREATE INDEX events_idx ON rideEvents (rideId, created, 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() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 25 | 26 | $this->addSql('DROP INDEX events_idx ON rideEvents'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | tests 19 | 20 | 21 | 22 | 23 | 24 | src 25 | 26 | src/*Bundle/Resources 27 | src/*/*Bundle/Resources 28 | src/*/Bundle/*Bundle/Resources 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Kata-Tasks.md: -------------------------------------------------------------------------------- 1 | Kata Tasks 2 | ========== 3 | 4 | ## Tutorial Plan 5 | 6 | ### Users Basic 7 | 8 | * UserRepositoryTest 9 | * Create / Get User 10 | * Assign Passenger Role 11 | 12 | * UserServiceTest 13 | * Create / Get User 14 | 15 | * RegisterUserCest 16 | * POST /register-user 17 | * first: 'fist name' 18 | * last: 'last name' 19 | 20 | * Doctrine Diff & Migrate 21 | * users, roles, users_roles 22 | 23 | 24 | ### Users & Roles 25 | 26 | * UserRepositoryTest 27 | * Assign Driver Role 28 | 29 | * AssignRoleToUserCest 30 | * PATCH /user/{id} 31 | role: 'Passenger' 32 | * PATCH /user/{id} 33 | role: 'Driver' 34 | 35 | * Migration 36 | * roles: 37 | 1 - Passenger 38 | 2 - Driver 39 | 40 | ### Locations & Rides 41 | 42 | * LocationRepositoryTest & LocationServiceTest 43 | * getOrCreateLocation 44 | 45 | * RideRepositoryTest & RideServiceTest 46 | * newRide($departure, $passenger) 47 | 48 | * CreateNewRideCest 49 | * POST /ride 50 | * departure [37.773160, -122.432444] 51 | * passengerId 52 | 53 | * AssignDestinationCest 54 | 55 | * AssignDriverCest 56 | 57 | -------------------------------------------------------------------------------- /template-phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | tests 21 | 22 | 23 | 24 | 25 | 26 | src 27 | 28 | src/*Bundle/Resources 29 | src/*/*Bundle/Resources 30 | src/*/Bundle/*Bundle/Resources 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/DoctrineMigrations/Version20180208222831.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 20 | 21 | $this->addSql('ALTER TABLE rides ADD created DATETIME DEFAULT NULL'); 22 | } 23 | 24 | /** 25 | * @param Schema $schema 26 | */ 27 | public function down(Schema $schema) 28 | { 29 | // this down() migration is auto-generated, please modify it to your needs 30 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 31 | 32 | $this->addSql('ALTER TABLE rides DROP created'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /web/app_dev.php: -------------------------------------------------------------------------------- 1 | loadClassCache(); 27 | $request = Request::createFromGlobals(); 28 | $response = $kernel->handle($request); 29 | $response->send(); 30 | $kernel->terminate($request, $response); 31 | -------------------------------------------------------------------------------- /app/DoctrineMigrations/Version20180212012515.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('ALTER TABLE users CHANGE first_name first_name VARCHAR(255) DEFAULT NULL, CHANGE last_name last_name VARCHAR(255) DEFAULT NULL'); 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() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 25 | 26 | $this->addSql('ALTER TABLE users CHANGE first_name first_name VARCHAR(255) NOT NULL COLLATE utf8_unicode_ci, CHANGE last_name last_name VARCHAR(255) NOT NULL COLLATE utf8_unicode_ci'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/DoctrineMigrations/Version20180212012516.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql( 19 | "INSERT INTO `oauth2_clients` VALUES (NULL, '3bcbxd9e24g0gk4swg0kwgcwg4o8k8g4g888kwc44gcc0gwwk4', 'a:1:{i:0;s:22:\"http://localhost:8000/\";}', '4ok2x70rlfokc8g0wws8c8kwcokw80k44sg48goc0ok4w0so0k', 'a:2:{i:0;s:8:\"password\";i:1;s:5:\"token\";}');" 20 | ); 21 | } 22 | 23 | public function down(Schema $schema) 24 | { 25 | // this down() migration is auto-generated, please modify it to your needs 26 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 27 | 28 | $this->addSql('delete from oauth2_clients'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/DoctrineMigrations/Version20180131035822.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 22 | 23 | $this->addSql('INSERT INTO `roles` (`id`, `name`) VALUES (1, \'Driver\'),(2, \'Passenger\');'); 24 | } 25 | 26 | /** 27 | * @param Schema $schema 28 | * @throws \Doctrine\DBAL\Migrations\AbortMigrationException 29 | */ 30 | public function down(Schema $schema) 31 | { 32 | // this down() migration is auto-generated, please modify it to your needs 33 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 34 | $this->addSql('delete from `roles`;'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/AppBundle/Repository/RideRepository.php: -------------------------------------------------------------------------------- 1 | assignDestination($destination); 16 | $this->save($ride); 17 | } 18 | 19 | public function saveRide(Ride $ride): void 20 | { 21 | $this->save($ride); 22 | } 23 | 24 | /** 25 | * @param Uuid $id 26 | * @return Ride 27 | * @throws RideNotFoundException 28 | */ 29 | public function getRideById(Uuid $id): Ride 30 | { 31 | try { 32 | return $this->em->createQuery( 33 | 'select r from E:Ride r where r.id = :id' 34 | ) 35 | ->setParameter('id', $id) 36 | ->getSingleResult(); 37 | } catch (\Exception $e) { 38 | throw new RideNotFoundException(); 39 | } 40 | } 41 | 42 | public function assignDriverToRide(Ride $ride, AppUser $driver): void 43 | { 44 | $ride->assignDriver($driver); 45 | $this->save($ride); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/AppRole.php: -------------------------------------------------------------------------------- 1 | id = $id; 36 | $this->name = $name; 37 | } 38 | 39 | public static function driver(): AppRole 40 | { 41 | return new self(self::DRIVER_ID, self::DRIVER); 42 | } 43 | 44 | public static function passenger(): AppRole 45 | { 46 | return new self(self::PASSENGER_ID, self::PASSENGER); 47 | } 48 | 49 | public static function isPassenger($role): bool 50 | { 51 | return $role === self::PASSENGER; 52 | } 53 | 54 | public static function isDriver($role): bool 55 | { 56 | return $role === self::DRIVER; 57 | } 58 | 59 | public function getId(): int 60 | { 61 | return $this->id; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/DoctrineMigrations/Version20181027033505.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E992FC23A8 ON users (username_canonical)'); 19 | $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9A0D96FBF ON users (email_canonical)'); 20 | $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9C05FB297 ON users (confirmation_token)'); 21 | } 22 | 23 | public function down(Schema $schema) : void 24 | { 25 | // this down() migration is auto-generated, please modify it to your needs 26 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 27 | 28 | $this->addSql('DROP INDEX UNIQ_1483A5E992FC23A8 ON users'); 29 | $this->addSql('DROP INDEX UNIQ_1483A5E9A0D96FBF ON users'); 30 | $this->addSql('DROP INDEX UNIQ_1483A5E9C05FB297 ON users'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/DoctrineMigrations/Version20180131035823.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 22 | 23 | $this->addSql(' 24 | INSERT INTO `rideEventTypes` (`id`, `name`) 25 | VALUES 26 | (1, \'Requested\'), 27 | (2, \'Accepted\'), 28 | (3,\'In Progress\'), 29 | (4,\'Cancelled\'), 30 | (5,\'Completed\'), 31 | (6,\'Rejected\') 32 | ; 33 | '); 34 | } 35 | 36 | /** 37 | * @param Schema $schema 38 | * @throws \Doctrine\DBAL\Migrations\AbortMigrationException 39 | */ 40 | public function down(Schema $schema) 41 | { 42 | // this down() migration is auto-generated, please modify it to your needs 43 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 44 | $this->addSql('delete from `rideEventTypes`;'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/AppBundle/Repository/LocationRepository.php: -------------------------------------------------------------------------------- 1 | em 21 | ->createQuery( 22 | 'SELECT l FROM E:AppLocation l WHERE l.lat = :lat AND l.long = :long' 23 | ) 24 | ->setParameter('lat', $lookupLocation->getLat()) 25 | ->setParameter('long', $lookupLocation->getLong()) 26 | ->getSingleResult(); 27 | } catch (NoResultException $e) { 28 | $this->save($lookupLocation); 29 | return $this->getLocation($lookupLocation); 30 | } catch (NonUniqueResultException $nur) { 31 | //TODO : possibly log what should be a rare occurrence 32 | //This would only happen if some rogue process inserts duplicate Locations 33 | //in the Data Store. 34 | return ($this->em->createQuery( 35 | 'SELECT l FROM E:AppLocation l WHERE l.lat = :lat AND l.long = :long order by l.created asc' 36 | ) 37 | ->setParameter('lat', $lookupLocation->getLat()) 38 | ->setParameter('long', $lookupLocation->getLong()) 39 | ->getResult())[0]; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/config/config_test.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: config.yml } 3 | 4 | #framework: 5 | # test: ~ 6 | # session: 7 | # storage_id: session.storage.mock_file 8 | # profiler: 9 | # collect: false 10 | 11 | framework: 12 | test: ~ 13 | secret: "%secret%" 14 | router: 15 | resource: "%kernel.root_dir%/config/routing_test.yml" 16 | strict_requirements: ~ 17 | form: ~ 18 | csrf_protection: ~ 19 | validation: { enable_annotations: true } 20 | #serializer: { enable_annotations: true } 21 | templating: 22 | engines: ['twig'] 23 | default_locale: "%locale%" 24 | trusted_hosts: ~ 25 | session: 26 | storage_id: session.storage.mock_file 27 | # http://symfony.com/doc/current/reference/configuration/framework.html#handler-id 28 | handler_id: ~ 29 | # save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%" 30 | fragments: ~ 31 | http_method_override: true 32 | assets: ~ 33 | php_errors: 34 | log: true 35 | profiler: 36 | collect: false 37 | 38 | 39 | security: 40 | encoders: 41 | AppBundle\Entity\AppUser: plaintext 42 | 43 | providers: 44 | in_memory: 45 | memory: ~ 46 | 47 | firewalls: 48 | # disables authentication for assets and the profiler, adapt it according to your needs 49 | dev: 50 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 51 | security: false 52 | 53 | doctrine: 54 | dbal: 55 | driver: pdo_sqlite 56 | 57 | web_profiler: 58 | toolbar: false 59 | intercept_redirects: false 60 | 61 | swiftmailer: 62 | disable_delivery: true 63 | -------------------------------------------------------------------------------- /app/config/config.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: parameters.yml } 3 | 4 | # Put parameters here that don't need to change on each machine where the app is deployed 5 | # http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 6 | parameters: 7 | locale: en 8 | 9 | # Twig Configuration 10 | twig: 11 | debug: "%kernel.debug%" 12 | strict_variables: "%kernel.debug%" 13 | 14 | # Doctrine Configuration 15 | doctrine: 16 | dbal: 17 | driver: pdo_mysql 18 | host: "%database_host%" 19 | port: "%database_port%" 20 | dbname: "%database_name%" 21 | user: "%database_user%" 22 | password: "%database_password%" 23 | charset: UTF8 24 | # if using pdo_sqlite as your database driver: 25 | # 1. add the path in parameters.yml 26 | # e.g. database_path: "%kernel.root_dir%/data/data.db3" 27 | # 2. Uncomment database_path in parameters.yml.dist 28 | # 3. Uncomment next line: 29 | # path: "%database_path%" 30 | types: 31 | uuid: Ramsey\Uuid\Doctrine\UuidType 32 | 33 | orm: 34 | auto_generate_proxy_classes: "%kernel.debug%" 35 | naming_strategy: doctrine.orm.naming_strategy.underscore 36 | auto_mapping: true 37 | 38 | # TODO: ADD 39 | mappings: 40 | AppBundle: 41 | mapping: true 42 | prefix: AppBundle\Entity 43 | alias: E 44 | 45 | # Swiftmailer Configuration 46 | swiftmailer: 47 | transport: "%mailer_transport%" 48 | host: "%mailer_host%" 49 | username: "%mailer_user%" 50 | password: "%mailer_password%" 51 | spool: { type: memory } -------------------------------------------------------------------------------- /tests/AppBundle/DTO/UserDtoTest.php: -------------------------------------------------------------------------------- 1 | newNamedUser( 15 | 'chris', 16 | 'holland' 17 | ))->toDto(); 18 | 19 | 20 | self::assertNotNull($userDto->id); 21 | self::assertFalse($userDto->isDriver); 22 | self::assertFalse($userDto->isPassenger); 23 | self::assertEquals('chris holland', $userDto->fullName); 24 | } 25 | 26 | public function testUserDtoDriver() 27 | { 28 | $driver = $this->newNamedUser('Joe', 'Driver'); 29 | self::assertTrue($driver->isNamed('Joe Driver')); 30 | $newFirstName = 'NotJoe'; 31 | $newLastName = 'NotDriver'; 32 | $driver->setFirstName($newFirstName); 33 | $driver->setLastName($newLastName); 34 | self::assertSame($newFirstName, $driver->getFirstName()); 35 | self::assertSame($newLastName, $driver->getLastName()); 36 | $driver->assignRole(AppRole::driver()); 37 | $userDto = $driver->toDto(); 38 | self::assertTrue($userDto->isDriver); 39 | self::assertFalse($userDto->isPassenger); 40 | self::assertSame($driver->getUsername(), $userDto->username); 41 | } 42 | 43 | public function testUserDtoPassenger() 44 | { 45 | $passenger = $this->newNamedUser('Bob', 'Passenger'); 46 | $passenger->assignRole(AppRole::passenger()); 47 | $userDto = $passenger->toDto(); 48 | self::assertTrue($userDto->isPassenger); 49 | self::assertFalse($userDto->isDriver); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/config/security.yml: -------------------------------------------------------------------------------- 1 | # To get started with security, check out the documentation: 2 | # http://symfony.com/doc/current/security.html 3 | security: 4 | encoders: 5 | AppBundle\Entity\AppUser: bcrypt 6 | 7 | # http://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded 8 | providers: 9 | in_memory: 10 | memory: ~ 11 | fos_userbundle: 12 | id: fos_user.user_provider.username 13 | 14 | firewalls: 15 | # disables authentication for assets and the profiler, adapt it according to your needs 16 | dev: 17 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 18 | security: false 19 | 20 | oauth_token: # Everyone can access the access token URL. 21 | pattern: ^/oauth/v2/token 22 | security: false 23 | 24 | api-public: 25 | pattern: ^/api/v1/register-user 26 | security: false 27 | 28 | api: 29 | pattern: ^/api/v1 # All URLs are protected 30 | fos_oauth: true # OAuth2 protected resource 31 | stateless: true # Do no set session cookies 32 | anonymous: false 33 | provider: fos_userbundle 34 | 35 | main: 36 | anonymous: ~ 37 | # activate different ways to authenticate 38 | 39 | # http_basic: ~ 40 | # http://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate 41 | # http://symfony.com/doc/current/cookbook/security/form_login_setup.html 42 | 43 | pattern: ^/ 44 | form_login: 45 | provider: fos_userbundle 46 | check_path: fos_user_security_check 47 | #csrf_provider: security.csrf.token_manager 48 | logout: true 49 | anonymous: true 50 | logout_on_user_change: true 51 | -------------------------------------------------------------------------------- /tests/AppBundle/DTO/RideDtoTest.php: -------------------------------------------------------------------------------- 1 | newNamedUser($firstName, $lastName); 19 | $passenger->assignRole(AppRole::passenger()); 20 | $home = new AppLocation( 21 | LocationApi::HOME_LOCATION_LAT, 22 | LocationApi::HOME_LOCATION_LONG 23 | ); 24 | $ride = new Ride( 25 | $passenger, 26 | $home 27 | ); 28 | $rideDto = $ride->toDto(); 29 | 30 | self::assertSame($passenger->getId()->toString(), $rideDto->passengerId); 31 | self::assertNull($rideDto->driverId); 32 | self::assertNull($rideDto->destination); 33 | } 34 | 35 | public function testRideDtoWithDriverAndDestination() 36 | { 37 | $passenger = $this->newNamedUser('Joe', 'Passenger'); 38 | $passenger->assignRole(AppRole::passenger()); 39 | $driver = $this->newNamedUser('Bob', 'Driver'); 40 | $driver->assignRole(AppRole::driver()); 41 | $home = new AppLocation( 42 | LocationApi::HOME_LOCATION_LAT, 43 | LocationApi::HOME_LOCATION_LONG 44 | ); 45 | $work = new AppLocation( 46 | LocationApi::WORK_LOCATION_LAT, 47 | LocationApi::WORK_LOCATION_LONG 48 | ); 49 | $ride = new Ride( 50 | $passenger, 51 | $home 52 | ); 53 | $ride->assignDriver($driver); 54 | $ride->assignDestination($work); 55 | $rideDto = $ride->toDto(); 56 | 57 | self::assertSame($driver->getId()->toString(), $rideDto->driverId); 58 | self::assertTrue($work->isSameAs($rideDto->destination)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/AppBundle/Production/LocationApi.php: -------------------------------------------------------------------------------- 1 | locationRepository = new LocationRepository( 25 | $entityManager 26 | ); 27 | $this->locationService = new LocationService( 28 | $this->locationRepository 29 | ); 30 | } 31 | 32 | /** 33 | * @return LocationRepositoryInterface 34 | */ 35 | public function getRepo() 36 | { 37 | return $this->locationRepository; 38 | } 39 | 40 | /** 41 | * @return AppLocation 42 | */ 43 | public function getSavedHomeLocation() 44 | { 45 | return $this->locationService->getLocation( 46 | self::HOME_LOCATION_LAT, 47 | self::HOME_LOCATION_LONG 48 | ); 49 | } 50 | 51 | /** 52 | * @param $lat 53 | * @param $long 54 | * @return AppLocation 55 | */ 56 | public function getLocation($lat, $long) 57 | { 58 | return $this->locationService->getLocation( 59 | $lat, 60 | $long 61 | ); 62 | } 63 | 64 | public function getWorkLocation() 65 | { 66 | return $this->getLocation( 67 | self::WORK_LOCATION_LAT, 68 | self::WORK_LOCATION_LONG 69 | ); 70 | } 71 | 72 | /** 73 | * @return LocationService 74 | */ 75 | public function getService() : LocationService 76 | { 77 | return $this->locationService; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/AppBundle/Repository/RideEventRepository.php: -------------------------------------------------------------------------------- 1 | em->createQuery( 22 | 'select e from E:RideEvent e where e.ride = :ride order by e.created desc, e.id desc' 23 | ) 24 | ->setMaxResults(1) 25 | ->setParameter('ride', $ride) 26 | ->getSingleResult(); 27 | } catch (\Exception $e) { 28 | throw new RideNotFoundException(); 29 | } 30 | } 31 | 32 | public function markRideStatusByActor( 33 | Ride $ride, 34 | AppUser $actor, 35 | RideEventType $status 36 | ): RideEvent { 37 | $newEvent = new RideEvent( 38 | $ride, 39 | $actor, 40 | $this->getStatusReference($status) 41 | ); 42 | 43 | $this->save($newEvent); 44 | 45 | return $newEvent; 46 | } 47 | 48 | public function markRideStatusByPassenger(Ride $ride, RideEventType $status): RideEvent 49 | { 50 | $passengerEvent = $ride->getPassengerTransaction($this->getStatusReference($status)); 51 | $this->save($passengerEvent); 52 | return $passengerEvent; 53 | } 54 | 55 | /** 56 | * @param RideEventType $status 57 | * @return RideEventType 58 | */ 59 | private function getStatusReference(RideEventType $status) : RideEventType 60 | { 61 | /** @var RideEventType $status */ 62 | $status = $this->em->getRepository( 63 | RideEventType::class 64 | )->find($status->getId()); 65 | 66 | return $status; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/RideEvent.php: -------------------------------------------------------------------------------- 1 | ride = $ride; 67 | $this->actor = $actor; 68 | $this->type = $type; 69 | $this->created = new \DateTime(); 70 | } 71 | 72 | public function getId(): int 73 | { 74 | return $this->id; 75 | } 76 | 77 | public function is(RideEventType $typeToCompare): bool 78 | { 79 | return $this->type->equals($typeToCompare); 80 | } 81 | 82 | public function getStatus(): RideEventType 83 | { 84 | return $this->type; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/AppBundle/Ride/RideRepositoryTest.php: -------------------------------------------------------------------------------- 1 | user()->getSavedUser(), 21 | $this->location()->getSavedHomeLocation() 22 | ); 23 | $this->verifyExceptionWithMessage( 24 | RideNotFoundException::class, 25 | RideNotFoundException::MESSAGE 26 | ); 27 | 28 | $this->ride()->getRepoRideById($nonExistentRide->getId()); 29 | } 30 | 31 | /** 32 | * @throws DuplicateRoleAssignmentException 33 | * @throws UserNotFoundException 34 | * @throws UnauthorizedOperationException 35 | */ 36 | public function testCreateRideWithDepartureAndPassenger() 37 | { 38 | $ride = $this->ride()->getRepoSavedRide(); 39 | 40 | self::assertNotEmpty($ride->getId()); 41 | } 42 | 43 | /** 44 | * @throws DuplicateRoleAssignmentException 45 | * @throws UserNotFoundException 46 | * @throws UnauthorizedOperationException 47 | */ 48 | public function testAssignDestinationToRide() 49 | { 50 | $retrievedRide = $this->ride()->getRepoRideWithDestination(); 51 | 52 | self::assertTrue($retrievedRide->isDestinedFor($this->location()->getWorkLocation())); 53 | } 54 | 55 | /** 56 | * @throws DuplicateRoleAssignmentException 57 | * @throws UserNotFoundException 58 | * @throws UnauthorizedOperationException 59 | */ 60 | public function testAssignDriverToRide() 61 | { 62 | /** @var AppUser $driver */ 63 | $driver = $this->user()->getSavedUserWithName('Jamie', 'Isaacs'); 64 | $rideWithDestination = $this->ride()->getRepoRideWithDestination(); 65 | 66 | $this->ride()->assignRepoDriverToRide($rideWithDestination, $driver); 67 | $retrievedRide = $this->ride()->getRepoRideById($rideWithDestination->getId()); 68 | 69 | self::assertTrue($retrievedRide->isDrivenBy($driver)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/RideEventType.php: -------------------------------------------------------------------------------- 1 | id = $id; 45 | $this->name = $name; 46 | } 47 | 48 | public static function requested(): RideEventType 49 | { 50 | return new self(self::REQUESTED_ID, self::REQUESTED); 51 | } 52 | 53 | public static function accepted(): RideEventType 54 | { 55 | return new self(self::ACCEPTED_ID, self::ACCEPTED); 56 | } 57 | 58 | public static function inProgress(): RideEventType 59 | { 60 | return new self(self::IN_PROGRESS_ID, self::IN_PROGRESS_STATUS); 61 | } 62 | 63 | public static function cancelled(): RideEventType 64 | { 65 | return new self(self::CANCELLED_ID, self::CANCELLED); 66 | } 67 | 68 | public static function completed(): RideEventType 69 | { 70 | return new self(self::COMPLETED_ID, self::COMPLETED); 71 | } 72 | 73 | public static function rejected(): RideEventType 74 | { 75 | return new self(self::REJECTED_ID, self::REJECTED); 76 | } 77 | 78 | public static function newById($eventTypeId): RideEventType 79 | { 80 | return new self($eventTypeId, ''); 81 | } 82 | 83 | public function equals(RideEventType $typeToCompare): bool 84 | { 85 | return $this->id === $typeToCompare->id; 86 | } 87 | 88 | public function getId(): int 89 | { 90 | return $this->id; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/AppLocation.php: -------------------------------------------------------------------------------- 1 | id = Uuid::uuid4(); 52 | $this->lat = $lat; 53 | $this->long = $long; 54 | $this->created = new \DateTime(); 55 | } 56 | 57 | /** 58 | * @param AppLocation $toClone 59 | * @return AppLocation 60 | * @throws \Exception 61 | */ 62 | public static function cloneFrom(AppLocation $toClone): AppLocation 63 | { 64 | return new self($toClone->lat, $toClone->long); 65 | } 66 | 67 | public function getLat(): float 68 | { 69 | return $this->lat; 70 | } 71 | 72 | public function getLong(): float 73 | { 74 | return $this->long; 75 | } 76 | 77 | public function isSameAs(AppLocation $compareLocation): bool 78 | { 79 | return ( 80 | ($compareLocation->lat === $this->lat) 81 | && 82 | ($compareLocation->long === $this->long) 83 | ); 84 | } 85 | 86 | public function preDates(AppLocation $compareLocation): bool 87 | { 88 | return $this->created < $compareLocation->created; 89 | } 90 | 91 | public function equals(AppLocation $compareLocation): bool 92 | { 93 | return $this->id->equals($compareLocation->id); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/UserController.php: -------------------------------------------------------------------------------- 1 | user()->newUser( 24 | $request->get('firstName'), 25 | $request->get('lastName'), 26 | $request->get('email'), 27 | $request->get('username'), 28 | $request->get('password') 29 | )->toDto(); 30 | } 31 | 32 | /** 33 | * @Rest\Get("/api/v1/user/{id}") 34 | * @param string $id 35 | * @return UserDto 36 | * @throws UserNotFoundException 37 | * @throws UnauthorizedOperationException 38 | */ 39 | public function idAction(string $id): UserDto 40 | { 41 | return $this->getUserById($id)->toDto(); 42 | } 43 | 44 | /** 45 | * @Rest\Patch("/api/v1/user/{id}") 46 | * @param string $id 47 | * @param Request $request 48 | * @return UserDto 49 | * @throws UserNotFoundException 50 | * @throws DuplicateRoleAssignmentException 51 | * @throws UnauthorizedOperationException 52 | */ 53 | public function patchAction(string $id, Request $request): UserDto 54 | { 55 | $userToPatch = $this->getUserById($id); 56 | $this->patchRole($request, $userToPatch); 57 | return $userToPatch->toDto(); 58 | } 59 | 60 | /** 61 | * @param Request $request 62 | * @param $userToPatch 63 | * @throws DuplicateRoleAssignmentException 64 | * @throws UnauthorizedOperationException 65 | */ 66 | private function patchRole(Request $request, $userToPatch): void 67 | { 68 | $roleToAssign = $request->get('role'); 69 | if (AppRole::isPassenger($roleToAssign)) { 70 | $this->user()->makeUserPassenger($userToPatch); 71 | } elseif (AppRole::isDriver($roleToAssign)) { 72 | $this->user()->makeUserDriver($userToPatch); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/AppKernel.php: -------------------------------------------------------------------------------- 1 | getEnvironment(), ['test'], true)) { 23 | $bundles[] = new FOS\RestBundle\FOSRestBundle(); 24 | $bundles[] = new JMS\SerializerBundle\JMSSerializerBundle(); 25 | $bundles[] = new Nelmio\CorsBundle\NelmioCorsBundle(); 26 | $bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle(); 27 | $bundles[] = new FOS\UserBundle\FOSUserBundle(); 28 | $bundles[] = new FOS\OAuthServerBundle\FOSOAuthServerBundle(); 29 | } 30 | 31 | if (in_array($this->getEnvironment(), ['dev', 'test'], true)) { 32 | $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); 33 | $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); 34 | $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); 35 | $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); 36 | } 37 | 38 | return $bundles; 39 | } 40 | 41 | public function getRootDir() 42 | { 43 | return __DIR__; 44 | } 45 | 46 | public function getCacheDir() 47 | { 48 | return dirname(__DIR__).'/var/cache/'.$this->getEnvironment(); 49 | } 50 | 51 | public function getLogDir() 52 | { 53 | return dirname(__DIR__).'/var/logs'; 54 | } 55 | 56 | /** 57 | * @param LoaderInterface $loader 58 | * @throws Exception 59 | */ 60 | public function registerContainerConfiguration(LoaderInterface $loader) 61 | { 62 | $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/AppBundle/Location/LocationRepositoryTest.php: -------------------------------------------------------------------------------- 1 | getSavedLocation(); 13 | 14 | self::assertNotNull($homeLocation); 15 | } 16 | 17 | public function testGetDupeLocationsReturnsFirst() 18 | { 19 | $homeLocation = $this->getSavedLocation(); 20 | $dupeHomeLocation = $this->getSavedLocation(); 21 | self::assertTrue($homeLocation->preDates($dupeHomeLocation)); 22 | self::assertTrue($homeLocation->isSameAs($dupeHomeLocation)); 23 | self::assertFalse($dupeHomeLocation->equals($homeLocation)); 24 | $lookupLocation = AppLocation::cloneFrom($homeLocation); 25 | 26 | $retrievedLocation = $this->getOrCreateLocation($lookupLocation); 27 | 28 | self::assertTrue($homeLocation->isSameAs($retrievedLocation)); 29 | self::assertTrue($dupeHomeLocation->isSameAs($retrievedLocation)); 30 | self::assertTrue($homeLocation->equals($retrievedLocation)); 31 | self::assertFalse($dupeHomeLocation->equals($retrievedLocation)); 32 | } 33 | 34 | public function testGetExistingLocationByLatLong() 35 | { 36 | $savedLocation = $this->getSavedLocation(); 37 | $lookupLocation = AppLocation::cloneFrom($savedLocation); 38 | 39 | $retrievedLocation = $this->getOrCreateLocation($lookupLocation); 40 | 41 | self::assertTrue($retrievedLocation->isSameAs($savedLocation)); 42 | } 43 | 44 | public function testCreateAndGetNewLocation() 45 | { 46 | $workLocation = new AppLocation( 47 | LocationApi::WORK_LOCATION_LAT, 48 | LocationApi::WORK_LOCATION_LONG 49 | ); 50 | 51 | $retrievedLocation = $this->getOrCreateLocation($workLocation); 52 | 53 | self::assertTrue($retrievedLocation->isSameAs($workLocation)); 54 | } 55 | 56 | /** 57 | * @return AppLocation 58 | */ 59 | private function getSavedLocation() 60 | { 61 | $homeLocation = new AppLocation( 62 | LocationApi::HOME_LOCATION_LAT, 63 | LocationApi::HOME_LOCATION_LONG 64 | ); 65 | 66 | $this->save($homeLocation); 67 | 68 | return $homeLocation; 69 | } 70 | 71 | protected function getOrCreateLocation(AppLocation $lookupLocation) 72 | { 73 | return $this->location()->getRepo()->getLocation($lookupLocation); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TDD Kata with Symfony and Doctrine 2 | ================================== 3 | 4 | ## Introduction: 5 | 6 | * Consult ride-hailing.svg to digest some key application concepts. 7 | * Consult Kata-Tasks.md to get an idea of the various tests you'll be writing and help shape your sequencing. 8 | * With this said, you do not have to follow the sequencing outlined. 9 | 10 | ## Initial Set-Up 11 | 12 | What you need: 13 | 14 | * php 7.2 15 | * mysql 5.7 16 | * composer 17 | 18 | Possible OS X Installation: (Adapt to your OS) 19 | 20 | * ( Install Brew: https://brew.sh ) 21 | * brew unlink php@5.6 (if you already have php56) 22 | * You can later undo this if you wish: 23 | * brew unlink php@7.2 24 | * brew link php@5.6 25 | * brew link php@7.2 26 | * or brew install php@7.2 27 | * brew install sqlite 28 | * brew install mysql@5.7 29 | * brew install composer 30 | 31 | Checkout Code: 32 | 33 | * git clone https://github.com/elchris/kata_tdd_php_symfony.git 34 | * cd kata_tdd_php_symfony 35 | * switch to **clean-slate-with-acceptance** branch 36 | * git checkout **clean-slate-with-acceptance** 37 | * create new working branch from **clean-slate-with-acceptance** 38 | * git branch kata-run-1 39 | * git checkout kata-run-1 40 | 41 | Configure DB: 42 | 43 | * cd app/config 44 | * cp parameters.yml.dist parameters.yml 45 | * mysql.server start 46 | * log into mysql 47 | * create database symfony; 48 | 49 | Run: 50 | 51 | * cd ../.. 52 | * composer install 53 | * vendor/bin/phpunit 54 | * bin/console server:start 55 | * vendor/bin/codecept run 56 | 57 | ## References 58 | 59 | Migrations: 60 | 61 | * Generate a single migration from the current state of the Entity Graph 62 | * bin/console doctrine:migrations:diff 63 | * Execute all current migrations 64 | * bin/console doctrine:migrations:migrate 65 | 66 | Generate: 67 | 68 | * vendor/bin/codecept generate:suite api 69 | * vendor/bin/codecept generate:cept api CreateUser 70 | 71 | References: 72 | 73 | * https://www.jetbrains.com/help/phpstorm/testing-with-codeception.html 74 | * https://www.jetbrains.com/help/idea/testing-with-codeception.html 75 | * https://laravel.com/docs/5.5/homestead#first-steps 76 | * https://gist.github.com/diegonobre/341eb7b793fc841c0bba3f2b865b8d66 77 | 78 | Alternatives: 79 | 80 | * https://github.com/msgphp/user-bundle 81 | 82 | Testing: 83 | 84 | * Implicit: 85 | * http://localhost:8000/oauth/v2/auth?client_id=1_3bcbxd9e24g0gk4swg0kwgcwg4o8k8g4g888kwc44gcc0gwwk4&redirect_uri=http://localhost:8000/&response_type=token 86 | 87 | Issues: 88 | 89 | * https://youtrack.jetbrains.com/issue/WI-40950 90 | * https://github.com/doctrine/doctrine2/issues/7306 91 | 92 | 93 | Stats: 94 | * [![](http://codescene.io/projects/2090/status.svg) Get more details at **codescene.io**.](http://codescene.io/projects/2090/jobs/latest-successful/results) 95 | -------------------------------------------------------------------------------- /tests/AppBundle/User/UserServiceTest.php: -------------------------------------------------------------------------------- 1 | user()->getSavedUser(); 16 | self::assertTrue($user->isNamed('chris holland')); 17 | } 18 | 19 | /** 20 | * @throws DuplicateRoleAssignmentException 21 | * @throws UnauthorizedOperationException 22 | */ 23 | public function testRogueUserRoleAssignmentException() 24 | { 25 | $rogueUser = $this->user()->getSavedUserWithName('Rogue', 'User'); 26 | $authenticatedUser = $this->user()->getSavedUserWithName('Authenticated', 'User'); 27 | $this->user()->setAuthenticatedUser($authenticatedUser); 28 | 29 | $this->verifyExceptionWithMessage( 30 | UnauthorizedOperationException::class, 31 | UnauthorizedOperationException::MESSAGE 32 | ); 33 | $this->user()->makeUserPassenger($rogueUser); 34 | } 35 | 36 | /** 37 | * @throws UserNotFoundException 38 | * @throws UnauthorizedOperationException 39 | */ 40 | public function testRogueUserAccessException() 41 | { 42 | $rogueUser = $this->user()->getSavedUserWithName('Rogue', 'User'); 43 | $authenticatedUser = $this->user()->getSavedUserWithName('Authenticated', 'User'); 44 | $this->user()->setAuthenticatedUser($authenticatedUser); 45 | 46 | $this->verifyExceptionWithMessage( 47 | UnauthorizedOperationException::class, 48 | UnauthorizedOperationException::MESSAGE 49 | ); 50 | 51 | $this->user()->getServiceUserById($rogueUser->getId()); 52 | } 53 | 54 | /** 55 | * @throws DuplicateRoleAssignmentException 56 | * @throws UserNotFoundException 57 | * @throws UnauthorizedOperationException 58 | */ 59 | public function testMakeUserDriver() 60 | { 61 | $savedUser = $this->user()->getSavedUser(); 62 | $this->user()->makeUserDriver($savedUser); 63 | $retrievedUser = $this->user()->getServiceUserById($savedUser->getId()); 64 | 65 | self::assertTrue($retrievedUser->userHasRole(AppRole::driver())); 66 | } 67 | 68 | /** 69 | * @throws DuplicateRoleAssignmentException 70 | * @throws UserNotFoundException 71 | * @throws UnauthorizedOperationException 72 | */ 73 | public function testMakeUserPassenger() 74 | { 75 | $savedUser = $this->user()->getSavedUser(); 76 | $this->user()->makeUserPassenger($savedUser); 77 | $retrievedUser = $this->user()->getServiceUserById($savedUser->getId()); 78 | 79 | self::assertTrue($retrievedUser->userHasRole(AppRole::passenger())); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/AppBundle/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | userManager = $userManager; 26 | } 27 | 28 | /** 29 | * @param Uuid $userId 30 | * @return AppUser 31 | * @throws UserNotFoundException 32 | */ 33 | public function getUserById(Uuid $userId): AppUser 34 | { 35 | try { 36 | return $this->em->createQuery( 37 | 'select u from E:AppUser u where u.id = :userId' 38 | ) 39 | ->setParameter('userId', $userId) 40 | ->getSingleResult(); 41 | } catch (\Exception $e) { 42 | throw new UserNotFoundException(); 43 | } 44 | } 45 | 46 | /** 47 | * @param AppUser $user 48 | * @param AppRole $role 49 | * @throws DuplicateRoleAssignmentException 50 | */ 51 | public function assignRoleToUser(AppUser $user, AppRole $role): void 52 | { 53 | if ($user->userHasRole($role)) { 54 | throw new DuplicateRoleAssignmentException(); 55 | } 56 | $role = $this->getRoleReference($role); 57 | $user->assignRole($role); 58 | $this->save($user); 59 | } 60 | 61 | /** 62 | * @param AppUser $passedUser 63 | * @return AppUser 64 | */ 65 | public function saveNewUser(AppUser $passedUser): AppUser 66 | { 67 | /** @var AppUser $user */ 68 | $user = $this->userManager->createUser(); 69 | $user->setFirstName($passedUser->getFirstName()); 70 | $user->setLastName($passedUser->getLastName()); 71 | $user->setUsername($passedUser->getUsername()); 72 | $user->setEmail($passedUser->getEmail()); 73 | $user->setPlainPassword($passedUser->getPlainPassword()); 74 | $user->setEnabled(true); 75 | $this->userManager->updateUser($user); 76 | return $user; 77 | } 78 | 79 | /** 80 | * @param AppRole $role 81 | * @return null | AppRole 82 | */ 83 | private function getRoleReference(AppRole $role): AppRole 84 | { 85 | /** @var AppRole $role */ 86 | $role = $this->em->getRepository(AppRole::class)->findOneBy( 87 | [ 88 | 'id' => $role->getId() 89 | ] 90 | ); 91 | return $role; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/config/frameworks.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: security.yml } 3 | - { resource: services.yml } 4 | 5 | framework: 6 | #esi: ~ 7 | #translator: { fallbacks: ["%locale%"] } 8 | secret: "%secret%" 9 | router: 10 | resource: "%kernel.root_dir%/config/routing.yml" 11 | strict_requirements: ~ 12 | form: ~ 13 | csrf_protection: ~ 14 | validation: { enable_annotations: true } 15 | #serializer: { enable_annotations: true } 16 | templating: 17 | engines: ['twig'] 18 | default_locale: "%locale%" 19 | trusted_hosts: ~ 20 | session: 21 | # http://symfony.com/doc/current/reference/configuration/framework.html#handler-id 22 | handler_id: session.handler.native_file 23 | save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%" 24 | fragments: ~ 25 | http_method_override: true 26 | assets: ~ 27 | php_errors: 28 | log: true 29 | 30 | doctrine_migrations: 31 | dir_name: "%kernel.root_dir%/DoctrineMigrations" 32 | namespace: Application\Migrations 33 | table_name: migration_versions 34 | name: Application Migrations 35 | organize_migrations: false 36 | 37 | # Nelmio CORS Configuration 38 | nelmio_cors: 39 | defaults: 40 | allow_credentials: false 41 | allow_origin: ['*'] 42 | allow_headers: ['*'] 43 | allow_methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] 44 | max_age: 3600 45 | hosts: [] 46 | origin_regex: false 47 | 48 | # FOSRest Configuration 49 | fos_rest: 50 | routing_loader: 51 | default_format: json 52 | include_format: false 53 | exception: 54 | enabled: true 55 | body_listener: true 56 | format_listener: 57 | rules: 58 | - { path: '^/api|oauth/token', priorities: ['json'], fallback_format: json, prefer_extension: false } 59 | - { path: '^/login', priorities: ['html'], fallback_format: html, prefer_extension: false } 60 | - { path: '^/oauth/v2/auth', priorities: ['html'], fallback_format: html, prefer_extension: false } 61 | - { path: '^/', priorities: ['html'], fallback_format: html, prefer_extension: false } 62 | param_fetcher_listener: true 63 | view: 64 | view_response_listener: 'force' 65 | formats: 66 | json: true 67 | 68 | fos_user: 69 | db_driver: orm # other valid values are 'mongodb', 'couchdb' and 'propel' 70 | firewall_name: api 71 | user_class: AppBundle\Entity\AppUser 72 | from_email: 73 | address: "noreply@yourcompany.com" 74 | sender_name: "No Reply" 75 | 76 | fos_oauth_server: 77 | db_driver: orm 78 | client_class: AppBundle\Entity\Client 79 | access_token_class: AppBundle\Entity\AccessToken 80 | refresh_token_class: AppBundle\Entity\RefreshToken 81 | auth_code_class: AppBundle\Entity\AuthCode 82 | service: 83 | user_provider: fos_user.user_provider.username # This property will be used when valid credentials are given to load the user upon access token creation 84 | -------------------------------------------------------------------------------- /app/DoctrineMigrations/Version20180208222514.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 20 | 21 | $this->addSql('ALTER TABLE locations CHANGE id id CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\''); 22 | $this->addSql('ALTER TABLE users ADD created DATETIME DEFAULT NULL, CHANGE id id CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\''); 23 | $this->addSql('ALTER TABLE users_roles CHANGE userId userId CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\''); 24 | $this->addSql('ALTER TABLE rides CHANGE id id CHAR(36) NOT NULL COMMENT \'(DC2Type:uuid)\', CHANGE passengerId passengerId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', CHANGE driverId driverId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', CHANGE departureId departureId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', CHANGE destinationId destinationId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\''); 25 | $this->addSql('ALTER TABLE rideEvents CHANGE rideId rideId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', CHANGE userId userId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\''); 26 | } 27 | 28 | /** 29 | * @param Schema $schema 30 | */ 31 | public function down(Schema $schema) 32 | { 33 | // this down() migration is auto-generated, please modify it to your needs 34 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 35 | 36 | $this->addSql('ALTER TABLE locations CHANGE id id CHAR(36) NOT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\''); 37 | $this->addSql('ALTER TABLE rideEvents CHANGE rideId rideId CHAR(36) DEFAULT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', CHANGE userId userId CHAR(36) DEFAULT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\''); 38 | $this->addSql('ALTER TABLE rides CHANGE id id CHAR(36) NOT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', CHANGE passengerId passengerId CHAR(36) DEFAULT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', CHANGE driverId driverId CHAR(36) DEFAULT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', CHANGE departureId departureId CHAR(36) DEFAULT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\', CHANGE destinationId destinationId CHAR(36) DEFAULT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\''); 39 | $this->addSql('ALTER TABLE users DROP created, CHANGE id id CHAR(36) NOT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\''); 40 | $this->addSql('ALTER TABLE users_roles CHANGE userId userId CHAR(36) NOT NULL COLLATE utf8_unicode_ci COMMENT \'(DC2Type:guid)\''); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrisholland/kata_tdd_php_symfony", 3 | "type": "project", 4 | "description" : "Various exercises in Test-Driven Development of projects built in PHP with Symfony 3", 5 | "keywords": ["TDD", "kata", "symfony", "php"], 6 | "homepage": "https://github.com/elchris/kata_tdd_php_symfony", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Chris Holland", 11 | "email": "frenchy@gmail.com", 12 | "homepage": "http://www.linkedin.com/in/chrisholland", 13 | "role": "Developer" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "": "src/" 19 | }, 20 | "classmap": [ 21 | "app/AppKernel.php", 22 | "app/AppCache.php" 23 | ] 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Tests\\": "tests/" 28 | } 29 | }, 30 | "require": { 31 | "php": ">=7.2", 32 | "doctrine/orm": "^2.6", 33 | "doctrine/doctrine-bundle": "^1.6", 34 | "doctrine/doctrine-cache-bundle": "^1.3", 35 | "symfony/swiftmailer-bundle": "^2.3", 36 | "symfony/monolog-bundle": "^3.0", 37 | "symfony/polyfill-apcu": "^1.0", 38 | "sensio/distribution-bundle": "^5.0", 39 | "sensio/framework-extra-bundle": "^3.0.2", 40 | "incenteev/composer-parameter-handler": "^2.0", 41 | "doctrine/doctrine-migrations-bundle": "^1.3", 42 | "squizlabs/php_codesniffer": "*", 43 | "friendsofsymfony/rest-bundle": "^2.3", 44 | "jms/serializer-bundle": "^2.3", 45 | "nelmio/cors-bundle": "^1.5", 46 | "ramsey/uuid-doctrine": "^1.5", 47 | "symfony/symfony": "3.4.*", 48 | "friendsofsymfony/user-bundle": "^2.0", 49 | "friendsofsymfony/oauth-server-bundle": "^1.5" 50 | }, 51 | "require-dev": { 52 | "sensio/generator-bundle": "^3.0", 53 | "symfony/phpunit-bridge": "^4.2", 54 | "phpunit/phpunit": "^7.0", 55 | "codeception/codeception": "^2.4" 56 | }, 57 | "scripts": { 58 | "symfony-scripts": [ 59 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", 60 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", 61 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", 62 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", 63 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile", 64 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget" 65 | ], 66 | "post-install-cmd": [ 67 | "@symfony-scripts" 68 | ], 69 | "post-update-cmd": [ 70 | "@symfony-scripts" 71 | ] 72 | }, 73 | "extra": { 74 | "symfony-app-dir": "app", 75 | "symfony-bin-dir": "bin", 76 | "symfony-var-dir": "var", 77 | "symfony-web-dir": "web", 78 | "symfony-tests-dir": "tests", 79 | "symfony-assets-install": "relative", 80 | "incenteev-parameters": { 81 | "file": "app/config/parameters.yml" 82 | }, 83 | "branch-alias": null 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | # Use the front controller as index file. It serves as a fallback solution when 2 | # every other rewrite/redirect fails (e.g. in an aliased environment without 3 | # mod_rewrite). Additionally, this reduces the matching process for the 4 | # start page (path "/") because otherwise Apache will apply the rewriting rules 5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). 6 | DirectoryIndex app.php 7 | 8 | # By default, Apache does not evaluate symbolic links if you did not enable this 9 | # feature in your server configuration. Uncomment the following line if you 10 | # install assets as symlinks or if you experience problems related to symlinks 11 | # when compiling LESS/Sass/CoffeScript assets. 12 | # Options FollowSymlinks 13 | 14 | # Disabling MultiViews prevents unwanted negotiation, e.g. "/app" should not resolve 15 | # to the front controller "/app.php" but be rewritten to "/app.php/app". 16 | 17 | Options -MultiViews 18 | 19 | 20 | 21 | RewriteEngine On 22 | 23 | # Determine the RewriteBase automatically and set it as environment variable. 24 | # If you are using Apache aliases to do mass virtual hosting or installed the 25 | # project in a subdirectory, the base path will be prepended to allow proper 26 | # resolution of the app.php file and to redirect to the correct URI. It will 27 | # work in environments without path prefix as well, providing a safe, one-size 28 | # fits all solution. But as you do not need it in this case, you can comment 29 | # the following 2 lines to eliminate the overhead. 30 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ 31 | RewriteRule ^(.*) - [E=BASE:%1] 32 | 33 | # Sets the HTTP_AUTHORIZATION header removed by Apache 34 | RewriteCond %{HTTP:Authorization} . 35 | RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 36 | 37 | # Redirect to URI without front controller to prevent duplicate content 38 | # (with and without `/app.php`). Only do this redirect on the initial 39 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an 40 | # endless redirect loop (request -> rewrite to front controller -> 41 | # redirect -> request -> ...). 42 | # So in case you get a "too many redirects" error or you always get redirected 43 | # to the start page because your Apache does not expose the REDIRECT_STATUS 44 | # environment variable, you have 2 choices: 45 | # - disable this feature by commenting the following 2 lines or 46 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the 47 | # following RewriteCond (best solution) 48 | RewriteCond %{ENV:REDIRECT_STATUS} ^$ 49 | RewriteRule ^app\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] 50 | 51 | # If the requested filename exists, simply serve it. 52 | # We only want to let Apache serve files and not directories. 53 | RewriteCond %{REQUEST_FILENAME} -f 54 | RewriteRule ^ - [L] 55 | 56 | # Rewrite all other queries to the front controller. 57 | RewriteRule ^ %{ENV:BASE}/app.php [L] 58 | 59 | 60 | 61 | 62 | # When mod_rewrite is not available, we instruct a temporary redirect of 63 | # the start page to the front controller explicitly so that the website 64 | # and the generated links can still be used. 65 | RedirectMatch 302 ^/$ /app.php/ 66 | # RedirectTemp cannot be used instead 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/AppBundle/Service/UserService.php: -------------------------------------------------------------------------------- 1 | userRepository = $userRepository; 28 | } 29 | 30 | public function setAuthenticatedUser(AppUser $user): void 31 | { 32 | $this->authenticatedUser = $user; 33 | } 34 | 35 | /** 36 | * @param $firstName 37 | * @param $lastName 38 | * @param $email 39 | * @param $username 40 | * @param $password 41 | * @return AppUser 42 | * @throws \Exception 43 | */ 44 | public function newUser($firstName, $lastName, $email, $username, $password) : AppUser 45 | { 46 | $newUser = new AppUser($firstName, $lastName, $email, $username, $password); 47 | return $this->userRepository->saveNewUser($newUser); 48 | } 49 | 50 | /** 51 | * @param Uuid $userId 52 | * @return AppUser 53 | * @throws UserNotFoundException 54 | * @throws UnauthorizedOperationException 55 | */ 56 | public function getUserById(Uuid $userId) : AppUser 57 | { 58 | $this->verifyAuthenticatedId($userId); 59 | return $this->userRepository->getUserById($userId); 60 | } 61 | 62 | /** 63 | * @param AppUser $user 64 | * @throws DuplicateRoleAssignmentException 65 | * @throws UnauthorizedOperationException 66 | */ 67 | public function makeUserDriver(AppUser $user): void 68 | { 69 | $this->assignRole($user, AppRole::driver()); 70 | } 71 | 72 | /** 73 | * @param AppUser $user 74 | * @throws DuplicateRoleAssignmentException 75 | * @throws UnauthorizedOperationException 76 | */ 77 | public function makeUserPassenger(AppUser $user): void 78 | { 79 | $this->assignRole($user, AppRole::passenger()); 80 | } 81 | 82 | /** 83 | * @param AppUser $user 84 | * @param AppRole $role 85 | * @throws DuplicateRoleAssignmentException 86 | * @throws UnauthorizedOperationException 87 | */ 88 | private function assignRole(AppUser $user, AppRole $role): void 89 | { 90 | $this->verifyAuthenticatedUser($user); 91 | $this->userRepository->assignRoleToUser($user, $role); 92 | } 93 | 94 | /** 95 | * @param Uuid $userId 96 | * @throws UnauthorizedOperationException 97 | */ 98 | private function verifyAuthenticatedId(Uuid $userId): void 99 | { 100 | if (!$this->authenticatedUser->getId()->equals($userId)) { 101 | throw new UnauthorizedOperationException(); 102 | } 103 | } 104 | 105 | /** 106 | * @param AppUser $user 107 | * @throws UnauthorizedOperationException 108 | */ 109 | private function verifyAuthenticatedUser(AppUser $user): void 110 | { 111 | if (!$user->is($this->authenticatedUser)) { 112 | throw new UnauthorizedOperationException(); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/AppBundle/AppTestCase.php: -------------------------------------------------------------------------------- 1 | em = static::$kernel->getContainer()->get('doctrine')->getManager(); 40 | $this->userManager = new FakeUserManager($this->em()); 41 | $this->setUpEntityManager(); 42 | 43 | $this->ride()->bootStrapRideEventTypes(); 44 | $this->user()->bootStrapRoles(); 45 | } 46 | 47 | protected function em() 48 | { 49 | return $this->em; 50 | } 51 | 52 | protected function save($entity) 53 | { 54 | $this->em->persist($entity); 55 | $this->em->flush(); 56 | return $entity; 57 | } 58 | 59 | /** 60 | * @param $firstName 61 | * @param $lastName 62 | * @return AppUser 63 | */ 64 | protected function newNamedUser($firstName, $lastName): AppUser 65 | { 66 | return (new FakeUser($firstName, $lastName))->toEntity(); 67 | } 68 | 69 | private function setUpEntityManager() 70 | { 71 | $classes = $this->em()->getMetadataFactory()->getAllMetadata(); 72 | $tool = new SchemaTool($this->em); 73 | $tool->dropSchema($classes); 74 | try { 75 | $tool->createSchema($classes); 76 | } catch (ToolsException $e) { 77 | } 78 | } 79 | 80 | /** 81 | * @param string $class 82 | * @param string $message 83 | */ 84 | protected function verifyExceptionWithMessage(string $class, string $message): void 85 | { 86 | $this->expectException($class); 87 | $this->expectExceptionMessage($message); 88 | } 89 | 90 | /** 91 | * @return UserApi 92 | */ 93 | protected function user() 94 | { 95 | if (is_null($this->userApi)) { 96 | $this->userApi = new UserApi( 97 | $this->em(), 98 | $this->userManager 99 | ); 100 | } 101 | return $this->userApi; 102 | } 103 | 104 | /** 105 | * @return LocationApi 106 | */ 107 | protected function location() 108 | { 109 | if (is_null($this->locationApi)) { 110 | $this->locationApi = new LocationApi($this->em()); 111 | } 112 | return $this->locationApi; 113 | } 114 | 115 | /** 116 | * @return RideApi 117 | */ 118 | protected function ride() 119 | { 120 | if (is_null($this->rideApi)) { 121 | $this->rideApi = new RideApi( 122 | $this->em(), 123 | $this->user(), 124 | $this->location() 125 | ); 126 | } 127 | return $this->rideApi; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/Ride.php: -------------------------------------------------------------------------------- 1 | id = Uuid::uuid4(); 70 | $this->passenger = $passenger; 71 | $this->departure = $departure; 72 | $this->created = new \DateTime(null, new \DateTimeZone('UTC')); 73 | } 74 | 75 | public function getId(): Uuid 76 | { 77 | return $this->id; 78 | } 79 | 80 | public function assignDestination(AppLocation $destination): void 81 | { 82 | $this->destination = $destination; 83 | } 84 | 85 | public function hasDestination(): bool 86 | { 87 | return ! is_null($this->destination); 88 | } 89 | 90 | public function assignDriver(AppUser $driver): void 91 | { 92 | $this->driver = $driver; 93 | } 94 | 95 | public function isDrivenBy(AppUser $driver): bool 96 | { 97 | return $this->driver->is($driver); 98 | } 99 | 100 | public function hasDriver(): bool 101 | { 102 | return ! is_null($this->driver); 103 | } 104 | 105 | public function isDestinedFor(AppLocation $destinationLocation): bool 106 | { 107 | return $this->destination->isSameAs($destinationLocation); 108 | } 109 | 110 | public function getPassengerTransaction(RideEventType $status): RideEvent 111 | { 112 | return new RideEvent( 113 | $this, 114 | $this->passenger, 115 | $status 116 | ); 117 | } 118 | 119 | public function is(Ride $rideToCompare): bool 120 | { 121 | return $this->id->equals($rideToCompare->id); 122 | } 123 | 124 | public function toDto(): RideDto 125 | { 126 | return new RideDto( 127 | $this, 128 | $this->passenger, 129 | $this->driver, 130 | $this->destination 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/AppBundle/Ride/RideTransitionTest.php: -------------------------------------------------------------------------------- 1 | user()->getNewDriver(); 32 | $ride = $this->ride()->getSavedNewRideWithPassengerAndDestination(); 33 | 34 | $this->assertRidePatchEvent($ride, RideEventType::ACCEPTED_ID, $driver); 35 | $this->assertRidePatchEvent($ride, RideEventType::IN_PROGRESS_ID, $driver); 36 | $this->assertRidePatchEvent($ride, RideEventType::COMPLETED_ID, $driver); 37 | $this->assertRidePatchEvent($ride, null, $driver); 38 | } 39 | 40 | /** 41 | * @throws ActingDriverIsNotAssignedDriverException 42 | * @throws DuplicateRoleAssignmentException 43 | * @throws RideLifeCycleException 44 | * @throws RideNotFoundException 45 | * @throws UserNotFoundException 46 | * @throws UserNotInDriverRoleException 47 | * @throws UserNotInPassengerRoleException 48 | * @throws UnauthorizedOperationException 49 | */ 50 | public function testPatchRideLifeCycleNullDriverIdAndEventId() 51 | { 52 | $ride = $this->ride()->getSavedNewRideWithPassengerAndDestination(); 53 | $patchedRide = $this->ride()->updateRideByDriverAndEventId( 54 | $ride, 55 | null, 56 | null 57 | ); 58 | self::assertTrue( 59 | RideEventType::requested()->equals( 60 | $this->ride()->getRideStatus($patchedRide) 61 | ) 62 | ); 63 | } 64 | 65 | /** 66 | * @param Ride $ride 67 | * @param string $eventId |null 68 | * @param AppUser $driver 69 | * @return Ride 70 | * @throws ActingDriverIsNotAssignedDriverException 71 | * @throws RideLifeCycleException 72 | * @throws RideNotFoundException 73 | * @throws UserNotInDriverRoleException 74 | * @throws UserNotFoundException 75 | * @throws UnauthorizedOperationException 76 | */ 77 | private function assertRidePatchEvent(Ride $ride, string $eventId = null, AppUser $driver): Ride 78 | { 79 | $patchedRide = $this->ride()->updateRideByDriverAndEventId( 80 | $ride, 81 | $eventId, 82 | $driver->getId()->toString() 83 | ); 84 | 85 | self::assertTrue($ride->isDrivenBy($driver)); 86 | if (! is_null($eventId)) { 87 | self::assertTrue( 88 | RideEventType::newById(intval($eventId))->equals( 89 | $this->ride()->getRideStatus($patchedRide) 90 | ) 91 | ); 92 | } else { 93 | self::assertFalse( 94 | RideEventType::newById(intval($eventId))->equals( 95 | $this->ride()->getRideStatus($patchedRide) 96 | ) 97 | ); 98 | } 99 | return $patchedRide; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/AppBundle/Service/RideTransitionService.php: -------------------------------------------------------------------------------- 1 | rideService = $rideService; 31 | $this->userService = $userService; 32 | } 33 | 34 | /** 35 | * @param Ride $ride 36 | * @param string $eventId |null 37 | * @param string $driverId |null 38 | * @return Ride 39 | * @throws ActingDriverIsNotAssignedDriverException 40 | * @throws RideLifeCycleException 41 | * @throws RideNotFoundException 42 | * @throws UserNotFoundException 43 | * @throws UserNotInDriverRoleException 44 | * @throws UnauthorizedOperationException 45 | */ 46 | public function updateRideByDriverAndEventId(Ride $ride, string $eventId = null, string $driverId = null) : Ride 47 | { 48 | if (! is_null($driverId)) { 49 | /** @var Uuid $uuid */ 50 | $uuid = Uuid::fromString($driverId); 51 | $driver = $this->userService->getUserById($uuid); 52 | $eventToProcess = RideEventType::newById(intval($eventId)); 53 | $this->patchRideAcceptance($eventToProcess, $ride, $driver); 54 | $this->patchRideInProgress($eventToProcess, $ride, $driver); 55 | $this->patchRideCompleted($eventToProcess, $ride, $driver); 56 | } 57 | return $ride; 58 | } 59 | 60 | /** 61 | * @param RideEventType $eventToProcess 62 | * @param Ride $rideToPatch 63 | * @param AppUser $driver 64 | * @throws RideLifeCycleException 65 | * @throws RideNotFoundException 66 | * @throws UserNotInDriverRoleException 67 | */ 68 | private function patchRideAcceptance(RideEventType $eventToProcess, Ride $rideToPatch, AppUser $driver): void 69 | { 70 | if (RideEventType::accepted()->equals($eventToProcess)) { 71 | $this->rideService->acceptRide($rideToPatch, $driver); 72 | } 73 | } 74 | 75 | /** 76 | * @param RideEventType $eventToProcess 77 | * @param Ride $rideToPatch 78 | * @param AppUser $driver 79 | * @throws ActingDriverIsNotAssignedDriverException 80 | * @throws RideLifeCycleException 81 | * @throws RideNotFoundException 82 | * @throws UserNotInDriverRoleException 83 | */ 84 | private function patchRideInProgress(RideEventType $eventToProcess, Ride $rideToPatch, AppUser $driver): void 85 | { 86 | if (RideEventType::inProgress()->equals($eventToProcess)) { 87 | $this->rideService->markRideInProgress($rideToPatch, $driver); 88 | } 89 | } 90 | 91 | /** 92 | * @param RideEventType $eventToProcess 93 | * @param Ride $rideToPatch 94 | * @param AppUser $driver 95 | * @throws ActingDriverIsNotAssignedDriverException 96 | * @throws RideLifeCycleException 97 | * @throws RideNotFoundException 98 | * @throws UserNotInDriverRoleException 99 | */ 100 | private function patchRideCompleted(RideEventType $eventToProcess, Ride $rideToPatch, AppUser $driver): void 101 | { 102 | if (RideEventType::completed()->equals($eventToProcess)) { 103 | $this->rideService->markRideCompleted($rideToPatch, $driver); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/AppBundle/User/FakeUserManager.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 26 | } 27 | 28 | /** 29 | * Creates an empty user instance. 30 | * 31 | * @return UserInterface 32 | * @throws \Exception 33 | */ 34 | public function createUser() 35 | { 36 | $this->user = new AppUser(); 37 | return $this->user; 38 | } 39 | 40 | /** 41 | * Deletes a user. 42 | * 43 | * @param UserInterface $user 44 | */ 45 | public function deleteUser(UserInterface $user) 46 | { 47 | //no-op 48 | } 49 | 50 | /** 51 | * Finds one user by the given criteria. 52 | * 53 | * @param array $criteria 54 | * 55 | * @return UserInterface 56 | */ 57 | public function findUserBy(array $criteria) 58 | { 59 | return $this->user; 60 | } 61 | 62 | /** 63 | * Find a user by its username. 64 | * 65 | * @param string $username 66 | * 67 | * @return UserInterface or null if user does not exist 68 | */ 69 | public function findUserByUsername($username) 70 | { 71 | return $this->user; 72 | } 73 | 74 | /** 75 | * Finds a user by its email. 76 | * 77 | * @param string $email 78 | * 79 | * @return UserInterface or null if user does not exist 80 | */ 81 | public function findUserByEmail($email) 82 | { 83 | return $this->user; 84 | } 85 | 86 | /** 87 | * Finds a user by its username or email. 88 | * 89 | * @param string $usernameOrEmail 90 | * 91 | * @return UserInterface or null if user does not exist 92 | */ 93 | public function findUserByUsernameOrEmail($usernameOrEmail) 94 | { 95 | return $this->user; 96 | } 97 | 98 | /** 99 | * Finds a user by its confirmationToken. 100 | * 101 | * @param string $token 102 | * 103 | * @return UserInterface or null if user does not exist 104 | */ 105 | public function findUserByConfirmationToken($token) 106 | { 107 | return $this->user; 108 | } 109 | 110 | /** 111 | * Returns a collection with all user instances. 112 | * 113 | * @return \Traversable 114 | */ 115 | public function findUsers() 116 | { 117 | return new ArrayCollection([$this->user]); 118 | } 119 | 120 | /** 121 | * Returns the user's fully qualified class name. 122 | * 123 | * @return string 124 | */ 125 | public function getClass() 126 | { 127 | return ''; 128 | } 129 | 130 | /** 131 | * Reloads a user. 132 | * 133 | * @param UserInterface $user 134 | */ 135 | public function reloadUser(UserInterface $user) 136 | { 137 | //no-op 138 | } 139 | 140 | /** 141 | * Updates a user. 142 | * 143 | * @param UserInterface $user 144 | */ 145 | public function updateUser(UserInterface $user) 146 | { 147 | $this->entityManager->persist($user); 148 | $this->entityManager->flush(); 149 | } 150 | 151 | /** 152 | * Updates the canonical username and email fields for a user. 153 | * 154 | * @param UserInterface $user 155 | */ 156 | public function updateCanonicalFields(UserInterface $user) 157 | { 158 | //no-op 159 | } 160 | 161 | /** 162 | * Updates a user password if a plain password is set. 163 | * 164 | * @param UserInterface $user 165 | */ 166 | public function updatePassword(UserInterface $user) 167 | { 168 | //no-op 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/AppController.php: -------------------------------------------------------------------------------- 1 | container->get('fos_user.user_manager.public'); 43 | return $manager; 44 | } 45 | 46 | /** 47 | * @return UserService 48 | */ 49 | protected function user(): UserService 50 | { 51 | $authenticatedUser = $this->getUser(); 52 | if (is_null($this->userService)) { 53 | $this->userService = new UserService(new UserRepository( 54 | $this->em(), 55 | $this->getUserManager() 56 | )); 57 | if (! is_null($authenticatedUser)) { 58 | $this->userService->setAuthenticatedUser( 59 | $authenticatedUser 60 | ); 61 | } 62 | } 63 | return $this->userService; 64 | } 65 | 66 | /** 67 | * @return RideService 68 | */ 69 | protected function ride(): RideService 70 | { 71 | if (is_null($this->rideService)) { 72 | $this->rideService = new RideService( 73 | new RideRepository($this->em()), 74 | new RideEventRepository($this->em()) 75 | ); 76 | } 77 | 78 | return $this->rideService; 79 | } 80 | 81 | protected function rideTransition(): RideTransitionService 82 | { 83 | if (is_null($this->rideTransitionService)) { 84 | $this->rideTransitionService = new RideTransitionService( 85 | $this->ride(), 86 | $this->user() 87 | ); 88 | } 89 | 90 | return $this->rideTransitionService; 91 | } 92 | 93 | /** 94 | * @return LocationService 95 | */ 96 | protected function location(): LocationService 97 | { 98 | if (is_null($this->locationService)) { 99 | $this->locationService = new LocationService( 100 | new LocationRepository($this->em()) 101 | ); 102 | } 103 | 104 | return $this->locationService; 105 | } 106 | 107 | /** 108 | * @param string $id 109 | * @return Uuid 110 | */ 111 | protected function id(string $id): Uuid 112 | { 113 | /** @var Uuid $uuid */ 114 | $uuid = Uuid::fromString($id); 115 | return $uuid; 116 | } 117 | 118 | /** 119 | * @return EntityManagerInterface 120 | */ 121 | private function em(): EntityManagerInterface 122 | { 123 | /** @var EntityManagerInterface $em */ 124 | $em = $this->getDoctrine()->getManager(); 125 | return $em; 126 | } 127 | 128 | /** 129 | * @param string $id 130 | * @return AppUser 131 | * @throws UserNotFoundException 132 | * @throws UnauthorizedOperationException 133 | */ 134 | protected function getUserById(string $id): AppUser 135 | { 136 | return $this->user()->getUserById($this->id($id)); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/AppBundle/Controller/RideController.php: -------------------------------------------------------------------------------- 1 | getUserById( 33 | $request->get('passengerId') 34 | ); 35 | $departure = $this->location()->getLocation( 36 | $request->get('departureLat'), 37 | $request->get('departureLong') 38 | ); 39 | return $this 40 | ->ride() 41 | ->newRide($passenger, $departure) 42 | ->toDto() 43 | ; 44 | } 45 | 46 | /** 47 | * @Rest\Get("/api/v1/ride/{rideId}") 48 | * @param string $rideId 49 | * @return RideDto 50 | * @throws RideNotFoundException 51 | */ 52 | public function idAction(string $rideId): RideDto 53 | { 54 | return $this->getRide($rideId)->toDto(); 55 | } 56 | 57 | /** 58 | * @Rest\Get("/api/v1/ride/{rideId}/status") 59 | * @param string $rideId 60 | * @return RideEventType 61 | * @throws RideNotFoundException 62 | */ 63 | public function statusAction(string $rideId): RideEventType 64 | { 65 | return $this->ride()->getRideStatus( 66 | $this->getRide($rideId) 67 | ); 68 | } 69 | 70 | /** 71 | * @Rest\Patch("/api/v1/ride/{rideId}") 72 | * @param string $rideId 73 | * @param Request $request 74 | * @return RideDto 75 | * @throws RideNotFoundException 76 | * @throws UserNotFoundException 77 | * @throws RideLifeCycleException 78 | * @throws UserNotInDriverRoleException 79 | * @throws ActingDriverIsNotAssignedDriverException 80 | * @throws UnauthorizedOperationException 81 | * @throws \Exception 82 | */ 83 | public function patchAction(string $rideId, Request $request): RideDto 84 | { 85 | $rideToPatch = $this->getRide($rideId); 86 | $eventId = $request->get('eventId'); 87 | $driverId = $request->get('driverId'); 88 | $destinationLat = $request->get('destinationLat'); 89 | $destinationLong = $request->get('destinationLong'); 90 | $this->rideTransition()->updateRideByDriverAndEventId( 91 | $rideToPatch, 92 | $eventId, 93 | $driverId 94 | ); 95 | $this->patchRideDestination($destinationLat, $destinationLong, $rideToPatch); 96 | return $rideToPatch->toDto(); 97 | } 98 | 99 | /** 100 | * @param string $rideId 101 | * @return Ride 102 | * @throws RideNotFoundException 103 | */ 104 | private function getRide(string $rideId): Ride 105 | { 106 | return $this->ride()->getRide($this->id($rideId)); 107 | } 108 | 109 | /** 110 | * @param $destinationLat 111 | * @param $destinationLong 112 | * @param Ride $rideToPatch 113 | * @throws \Exception 114 | */ 115 | private function patchRideDestination($destinationLat, $destinationLong, Ride $rideToPatch): void 116 | { 117 | if (!is_null($destinationLat) && !is_null($destinationLong)) { 118 | $this->ride()->assignDestinationToRide( 119 | $rideToPatch, 120 | $this->location()->getLocation( 121 | floatval($destinationLat), 122 | floatval($destinationLong) 123 | ) 124 | ); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /bin/symfony_requirements: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getPhpIniConfigPath(); 9 | 10 | echo_title('Symfony Requirements Checker'); 11 | 12 | echo '> PHP is using the following php.ini file:'.PHP_EOL; 13 | if ($iniPath) { 14 | echo_style('green', ' '.$iniPath); 15 | } else { 16 | echo_style('yellow', ' WARNING: No configuration file (php.ini) used by PHP!'); 17 | } 18 | 19 | echo PHP_EOL.PHP_EOL; 20 | 21 | echo '> Checking Symfony requirements:'.PHP_EOL.' '; 22 | 23 | $messages = array(); 24 | foreach ($symfonyRequirements->getRequirements() as $req) { 25 | if ($helpText = get_error_message($req, $lineSize)) { 26 | echo_style('red', 'E'); 27 | $messages['error'][] = $helpText; 28 | } else { 29 | echo_style('green', '.'); 30 | } 31 | } 32 | 33 | $checkPassed = empty($messages['error']); 34 | 35 | foreach ($symfonyRequirements->getRecommendations() as $req) { 36 | if ($helpText = get_error_message($req, $lineSize)) { 37 | echo_style('yellow', 'W'); 38 | $messages['warning'][] = $helpText; 39 | } else { 40 | echo_style('green', '.'); 41 | } 42 | } 43 | 44 | if ($checkPassed) { 45 | echo_block('success', 'OK', 'Your system is ready to run Symfony projects'); 46 | } else { 47 | echo_block('error', 'ERROR', 'Your system is not ready to run Symfony projects'); 48 | 49 | echo_title('Fix the following mandatory requirements', 'red'); 50 | 51 | foreach ($messages['error'] as $helpText) { 52 | echo ' * '.$helpText.PHP_EOL; 53 | } 54 | } 55 | 56 | if (!empty($messages['warning'])) { 57 | echo_title('Optional recommendations to improve your setup', 'yellow'); 58 | 59 | foreach ($messages['warning'] as $helpText) { 60 | echo ' * '.$helpText.PHP_EOL; 61 | } 62 | } 63 | 64 | echo PHP_EOL; 65 | echo_style('title', 'Note'); 66 | echo ' The command console could use a different php.ini file'.PHP_EOL; 67 | echo_style('title', '~~~~'); 68 | echo ' than the one used with your web server. To be on the'.PHP_EOL; 69 | echo ' safe side, please check the requirements from your web'.PHP_EOL; 70 | echo ' server using the '; 71 | echo_style('yellow', 'web/config.php'); 72 | echo ' script.'.PHP_EOL; 73 | echo PHP_EOL; 74 | 75 | exit($checkPassed ? 0 : 1); 76 | 77 | function get_error_message(Requirement $requirement, $lineSize) 78 | { 79 | if ($requirement->isFulfilled()) { 80 | return; 81 | } 82 | 83 | $errorMessage = wordwrap($requirement->getTestMessage(), $lineSize - 3, PHP_EOL.' ').PHP_EOL; 84 | $errorMessage .= ' > '.wordwrap($requirement->getHelpText(), $lineSize - 5, PHP_EOL.' > ').PHP_EOL; 85 | 86 | return $errorMessage; 87 | } 88 | 89 | function echo_title($title, $style = null) 90 | { 91 | $style = $style ?: 'title'; 92 | 93 | echo PHP_EOL; 94 | echo_style($style, $title.PHP_EOL); 95 | echo_style($style, str_repeat('~', strlen($title)).PHP_EOL); 96 | echo PHP_EOL; 97 | } 98 | 99 | function echo_style($style, $message) 100 | { 101 | // ANSI color codes 102 | $styles = array( 103 | 'reset' => "\033[0m", 104 | 'red' => "\033[31m", 105 | 'green' => "\033[32m", 106 | 'yellow' => "\033[33m", 107 | 'error' => "\033[37;41m", 108 | 'success' => "\033[37;42m", 109 | 'title' => "\033[34m", 110 | ); 111 | $supports = has_color_support(); 112 | 113 | echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : ''); 114 | } 115 | 116 | function echo_block($style, $title, $message) 117 | { 118 | $message = ' '.trim($message).' '; 119 | $width = strlen($message); 120 | 121 | echo PHP_EOL.PHP_EOL; 122 | 123 | echo_style($style, str_repeat(' ', $width)); 124 | echo PHP_EOL; 125 | echo_style($style, str_pad(' ['.$title.']', $width, ' ', STR_PAD_RIGHT)); 126 | echo PHP_EOL; 127 | echo_style($style, $message); 128 | echo PHP_EOL; 129 | echo_style($style, str_repeat(' ', $width)); 130 | echo PHP_EOL; 131 | } 132 | 133 | function has_color_support() 134 | { 135 | static $support; 136 | 137 | if (null === $support) { 138 | if (DIRECTORY_SEPARATOR == '\\') { 139 | $support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI'); 140 | } else { 141 | $support = function_exists('posix_isatty') && @posix_isatty(STDOUT); 142 | } 143 | } 144 | 145 | return $support; 146 | } 147 | -------------------------------------------------------------------------------- /tests/AppBundle/User/UserRepositoryTest.php: -------------------------------------------------------------------------------- 1 | user()->getSavedUser(); 21 | $this->em()->clear(); 22 | $retrievedUser = $this->user()->getRepo()->getUserById($user->getId()); 23 | self::assertTrue($retrievedUser->is($user)); 24 | } 25 | 26 | public function testCreateAndSaveNewUser(): void 27 | { 28 | $user = $this->user()->getSavedUser(); 29 | 30 | self::assertNotNull($user); 31 | } 32 | 33 | /** 34 | * @throws UserNotFoundException 35 | */ 36 | public function testGetUserById(): void 37 | { 38 | $savedUser = $this->user()->getSavedUser(); 39 | 40 | $retrievedUser = $this->getRepoUserById($savedUser->getId()); 41 | 42 | self::assertTrue($savedUser->is($retrievedUser)); 43 | } 44 | 45 | /** 46 | * @throws UserNotFoundException 47 | * @throws Exception 48 | */ 49 | public function testBadUserIdThrowsUserNotFoundException(): void 50 | { 51 | /** @var Uuid $nonExistentId */ 52 | $nonExistentId = Uuid::uuid4(); 53 | 54 | $this->verifyExceptionWithMessage( 55 | UserNotFoundException::class, 56 | UserNotFoundException::MESSAGE 57 | ); 58 | $this->getRepoUserById($nonExistentId); 59 | } 60 | 61 | /** 62 | * @throws DuplicateRoleAssignmentException 63 | * @throws UserNotFoundException 64 | */ 65 | public function testAssignDriverRoleToUser(): void 66 | { 67 | $this->assertUserHasExpectedRole(AppRole::driver()); 68 | } 69 | 70 | /** 71 | * @throws DuplicateRoleAssignmentException 72 | * @throws UserNotFoundException 73 | */ 74 | public function testAssignPassengerRoleToUser(): void 75 | { 76 | $this->assertUserHasExpectedRole(AppRole::passenger()); 77 | } 78 | 79 | /** 80 | * @throws DuplicateRoleAssignmentException 81 | * @throws UserNotFoundException 82 | */ 83 | public function testUserCanHaveBothRoles(): void 84 | { 85 | $savedUser = $this->user()->getSavedUser(); 86 | 87 | $this->assignRepoRoleToUser($savedUser, AppRole::driver()); 88 | $this->assignRepoRoleToUser($savedUser, AppRole::passenger()); 89 | 90 | $retrievedUser = $this->getRepoUserById($savedUser->getId()); 91 | 92 | self::assertTrue($retrievedUser->userHasRole(AppRole::driver())); 93 | self::assertTrue($retrievedUser->userHasRole(AppRole::passenger())); 94 | } 95 | 96 | /** 97 | * @throws DuplicateRoleAssignmentException 98 | */ 99 | public function testDuplicateRoleAssignmentThrows(): void 100 | { 101 | $savedUser = $this->user()->getSavedUser(); 102 | 103 | $this->assignRepoRoleToUser($savedUser, AppRole::driver()); 104 | $this->verifyExceptionWithMessage( 105 | DuplicateRoleAssignmentException::class, 106 | DuplicateRoleAssignmentException::MESSAGE 107 | ); 108 | 109 | $this->assignRepoRoleToUser($savedUser, AppRole::driver()); 110 | } 111 | 112 | /** 113 | * @param AppRole $role 114 | * @throws DuplicateRoleAssignmentException 115 | * @throws UserNotFoundException 116 | */ 117 | private function assertUserHasExpectedRole(AppRole $role): void 118 | { 119 | $savedUser = $this->user()->getSavedUser(); 120 | 121 | $this->assignRepoRoleToUser($savedUser, $role); 122 | $retrievedUser = $this->getRepoUserById($savedUser->getId()); 123 | 124 | self::assertTrue($retrievedUser->userHasRole($role)); 125 | } 126 | 127 | /** 128 | * @param AppUser $user 129 | * @param AppRole $role 130 | * @throws DuplicateRoleAssignmentException 131 | */ 132 | protected function assignRepoRoleToUser(AppUser $user, AppRole $role): void 133 | { 134 | $this->user()->getRepo()->assignRoleToUser($user, $role); 135 | } 136 | 137 | /** 138 | * @param Uuid $userId 139 | * @return AppUser 140 | * @throws UserNotFoundException 141 | */ 142 | private function getRepoUserById(Uuid $userId): AppUser 143 | { 144 | return $this->user()->getRepo()->getUserById($userId); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/AppUser.php: -------------------------------------------------------------------------------- 1 | id = Uuid::uuid4(); 82 | $this->firstName = $firstName; 83 | $this->lastName = $lastName; 84 | $this->email = $email; 85 | $this->username = $username; 86 | $this->setPlainPassword($password); 87 | $this->appRoles = new ArrayCollection(); 88 | $this->created = new \DateTime(null, new \DateTimeZone('UTC')); 89 | } 90 | 91 | public function assignRole(AppRole $role): void 92 | { 93 | $this->appRoles->add($role); 94 | } 95 | 96 | public function userHasRole(AppRole $role): bool 97 | { 98 | $hasRoleCriteria = 99 | Criteria::create()->andWhere( 100 | Criteria::expr()->eq( 101 | 'id', 102 | $role->getId() 103 | ) 104 | ); 105 | return $this->appRoles->matching($hasRoleCriteria)->count() > 0; 106 | } 107 | 108 | public function isNamed(string $nameToCheck): bool 109 | { 110 | return $this->getFullName() === $nameToCheck; 111 | } 112 | 113 | public function is(AppUser $userToCompare): bool 114 | { 115 | return $this->getId()->equals($userToCompare->getId()); 116 | } 117 | 118 | public function toDto(): UserDto 119 | { 120 | return new UserDto( 121 | $this->id->toString(), 122 | $this->userHasRole(AppRole::driver()), 123 | $this->userHasRole(AppRole::passenger()), 124 | $this->getFullName(), 125 | $this->getUsername(), 126 | $this->getEmail() 127 | ); 128 | } 129 | 130 | /** 131 | * @return string 132 | */ 133 | private function getFullName(): string 134 | { 135 | return trim($this->firstName . ' ' . $this->lastName); 136 | } 137 | 138 | public function getFirstName(): string 139 | { 140 | return $this->firstName; 141 | } 142 | 143 | public function getLastName(): string 144 | { 145 | return $this->lastName; 146 | } 147 | 148 | /** 149 | * @param string $firstName 150 | */ 151 | public function setFirstName(string $firstName): void 152 | { 153 | $this->firstName = $firstName; 154 | } 155 | 156 | /** 157 | * @param string $lastName 158 | */ 159 | public function setLastName(string $lastName): void 160 | { 161 | $this->lastName = $lastName; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/Resources/views/default/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 |
5 |
6 |
7 |

Welcome to Symfony {{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') }}

8 |
9 | 10 |
11 |

12 | 13 | 14 | Your application is now ready. You can start working on it at: 15 | {{ base_dir }} 16 |

17 |
18 | 19 | 44 | 45 |
46 |
47 | {% endblock %} 48 | 49 | {% block stylesheets %} 50 | 76 | {% endblock %} 77 | -------------------------------------------------------------------------------- /app/DoctrineMigrations/Version20180211230225.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 17 | 18 | $this->addSql('CREATE TABLE oauth2_access_tokens (id INT AUTO_INCREMENT NOT NULL, client_id INT NOT NULL, user_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', token VARCHAR(255) NOT NULL, expires_at INT DEFAULT NULL, scope VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_D247A21B5F37A13B (token), INDEX IDX_D247A21B19EB6921 (client_id), INDEX IDX_D247A21BA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 19 | $this->addSql('CREATE TABLE oauth2_auth_codes (id INT AUTO_INCREMENT NOT NULL, client_id INT NOT NULL, user_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', token VARCHAR(255) NOT NULL, redirect_uri LONGTEXT NOT NULL, expires_at INT DEFAULT NULL, scope VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_A018A10D5F37A13B (token), INDEX IDX_A018A10D19EB6921 (client_id), INDEX IDX_A018A10DA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 20 | $this->addSql('CREATE TABLE oauth2_clients (id INT AUTO_INCREMENT NOT NULL, random_id VARCHAR(255) NOT NULL, redirect_uris LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', secret VARCHAR(255) NOT NULL, allowed_grant_types LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 21 | $this->addSql('CREATE TABLE oauth2_refresh_tokens (id INT AUTO_INCREMENT NOT NULL, client_id INT NOT NULL, user_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:uuid)\', token VARCHAR(255) NOT NULL, expires_at INT DEFAULT NULL, scope VARCHAR(255) DEFAULT NULL, UNIQUE INDEX UNIQ_D394478C5F37A13B (token), INDEX IDX_D394478C19EB6921 (client_id), INDEX IDX_D394478CA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 22 | $this->addSql('ALTER TABLE oauth2_access_tokens ADD CONSTRAINT FK_D247A21B19EB6921 FOREIGN KEY (client_id) REFERENCES oauth2_clients (id)'); 23 | $this->addSql('ALTER TABLE oauth2_access_tokens ADD CONSTRAINT FK_D247A21BA76ED395 FOREIGN KEY (user_id) REFERENCES users (id)'); 24 | $this->addSql('ALTER TABLE oauth2_auth_codes ADD CONSTRAINT FK_A018A10D19EB6921 FOREIGN KEY (client_id) REFERENCES oauth2_clients (id)'); 25 | $this->addSql('ALTER TABLE oauth2_auth_codes ADD CONSTRAINT FK_A018A10DA76ED395 FOREIGN KEY (user_id) REFERENCES users (id)'); 26 | $this->addSql('ALTER TABLE oauth2_refresh_tokens ADD CONSTRAINT FK_D394478C19EB6921 FOREIGN KEY (client_id) REFERENCES oauth2_clients (id)'); 27 | $this->addSql('ALTER TABLE oauth2_refresh_tokens ADD CONSTRAINT FK_D394478CA76ED395 FOREIGN KEY (user_id) REFERENCES users (id)'); 28 | $this->addSql('ALTER TABLE users ADD username VARCHAR(180) NOT NULL, ADD username_canonical VARCHAR(180) NOT NULL, ADD email VARCHAR(180) NOT NULL, ADD email_canonical VARCHAR(180) NOT NULL, ADD enabled TINYINT(1) NOT NULL, ADD salt VARCHAR(255) DEFAULT NULL, ADD password VARCHAR(255) NOT NULL, ADD last_login DATETIME DEFAULT NULL, ADD confirmation_token VARCHAR(180) DEFAULT NULL, ADD password_requested_at DATETIME DEFAULT NULL, ADD roles LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\''); 29 | $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E992FC23A8 ON users (username_canonical)'); 30 | $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9A0D96FBF ON users (email_canonical)'); 31 | $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9C05FB297 ON users (confirmation_token)'); 32 | } 33 | 34 | public function down(Schema $schema) 35 | { 36 | // this down() migration is auto-generated, please modify it to your needs 37 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 38 | 39 | $this->addSql('ALTER TABLE oauth2_access_tokens DROP FOREIGN KEY FK_D247A21B19EB6921'); 40 | $this->addSql('ALTER TABLE oauth2_auth_codes DROP FOREIGN KEY FK_A018A10D19EB6921'); 41 | $this->addSql('ALTER TABLE oauth2_refresh_tokens DROP FOREIGN KEY FK_D394478C19EB6921'); 42 | $this->addSql('DROP TABLE oauth2_access_tokens'); 43 | $this->addSql('DROP TABLE oauth2_auth_codes'); 44 | $this->addSql('DROP TABLE oauth2_clients'); 45 | $this->addSql('DROP TABLE oauth2_refresh_tokens'); 46 | $this->addSql('DROP INDEX UNIQ_1483A5E992FC23A8 ON users'); 47 | $this->addSql('DROP INDEX UNIQ_1483A5E9A0D96FBF ON users'); 48 | $this->addSql('DROP INDEX UNIQ_1483A5E9C05FB297 ON users'); 49 | $this->addSql('ALTER TABLE users DROP username, DROP username_canonical, DROP email, DROP email_canonical, DROP enabled, DROP salt, DROP password, DROP last_login, DROP confirmation_token, DROP password_requested_at, DROP roles'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/DoctrineMigrations/Version20180131035821.php: -------------------------------------------------------------------------------- 1 | abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 21 | 22 | $this->addSql('CREATE TABLE locations (id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', lat DOUBLE PRECISION NOT NULL, `longitude` DOUBLE PRECISION NOT NULL, created DATETIME NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 23 | $this->addSql('CREATE TABLE roles (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 24 | $this->addSql('CREATE TABLE users (id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 25 | $this->addSql('CREATE TABLE users_roles (userId CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', roleId INT NOT NULL, INDEX IDX_51498A8E64B64DCC (userId), INDEX IDX_51498A8EB8C2FD88 (roleId), PRIMARY KEY(userId, roleId)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 26 | $this->addSql('CREATE TABLE rides (id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', passengerId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', driverId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', departureId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', destinationId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', INDEX IDX_9D4620A3B0FAA905 (passengerId), INDEX IDX_9D4620A323F411D5 (driverId), INDEX IDX_9D4620A36E9E2929 (departureId), INDEX IDX_9D4620A3BF3434FC (destinationId), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 27 | $this->addSql('CREATE TABLE rideEvents (id INT AUTO_INCREMENT NOT NULL, created DATETIME NOT NULL, rideId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', userId CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\', eventTypeId INT DEFAULT NULL, INDEX IDX_893034F0F23620C7 (rideId), INDEX IDX_893034F064B64DCC (userId), INDEX IDX_893034F0577BCC16 (eventTypeId), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 28 | $this->addSql('CREATE TABLE rideEventTypes (id INT NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); 29 | $this->addSql('ALTER TABLE users_roles ADD CONSTRAINT FK_51498A8E64B64DCC FOREIGN KEY (userId) REFERENCES users (id)'); 30 | $this->addSql('ALTER TABLE users_roles ADD CONSTRAINT FK_51498A8EB8C2FD88 FOREIGN KEY (roleId) REFERENCES roles (id)'); 31 | $this->addSql('ALTER TABLE rides ADD CONSTRAINT FK_9D4620A3B0FAA905 FOREIGN KEY (passengerId) REFERENCES users (id)'); 32 | $this->addSql('ALTER TABLE rides ADD CONSTRAINT FK_9D4620A323F411D5 FOREIGN KEY (driverId) REFERENCES users (id)'); 33 | $this->addSql('ALTER TABLE rides ADD CONSTRAINT FK_9D4620A36E9E2929 FOREIGN KEY (departureId) REFERENCES locations (id)'); 34 | $this->addSql('ALTER TABLE rides ADD CONSTRAINT FK_9D4620A3BF3434FC FOREIGN KEY (destinationId) REFERENCES locations (id)'); 35 | $this->addSql('ALTER TABLE rideEvents ADD CONSTRAINT FK_893034F0F23620C7 FOREIGN KEY (rideId) REFERENCES rides (id)'); 36 | $this->addSql('ALTER TABLE rideEvents ADD CONSTRAINT FK_893034F064B64DCC FOREIGN KEY (userId) REFERENCES users (id)'); 37 | $this->addSql('ALTER TABLE rideEvents ADD CONSTRAINT FK_893034F0577BCC16 FOREIGN KEY (eventTypeId) REFERENCES rideEventTypes (id)'); 38 | } 39 | 40 | /** 41 | * @param Schema $schema 42 | * @throws \Doctrine\DBAL\Migrations\AbortMigrationException 43 | */ 44 | public function down(Schema $schema) 45 | { 46 | // this down() migration is auto-generated, please modify it to your needs 47 | $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); 48 | 49 | $this->addSql('ALTER TABLE rides DROP FOREIGN KEY FK_9D4620A36E9E2929'); 50 | $this->addSql('ALTER TABLE rides DROP FOREIGN KEY FK_9D4620A3BF3434FC'); 51 | $this->addSql('ALTER TABLE users_roles DROP FOREIGN KEY FK_51498A8EB8C2FD88'); 52 | $this->addSql('ALTER TABLE users_roles DROP FOREIGN KEY FK_51498A8E64B64DCC'); 53 | $this->addSql('ALTER TABLE rides DROP FOREIGN KEY FK_9D4620A3B0FAA905'); 54 | $this->addSql('ALTER TABLE rides DROP FOREIGN KEY FK_9D4620A323F411D5'); 55 | $this->addSql('ALTER TABLE rideEvents DROP FOREIGN KEY FK_893034F064B64DCC'); 56 | $this->addSql('ALTER TABLE rideEvents DROP FOREIGN KEY FK_893034F0F23620C7'); 57 | $this->addSql('ALTER TABLE rideEvents DROP FOREIGN KEY FK_893034F0577BCC16'); 58 | $this->addSql('DROP TABLE locations'); 59 | $this->addSql('DROP TABLE roles'); 60 | $this->addSql('DROP TABLE users'); 61 | $this->addSql('DROP TABLE users_roles'); 62 | $this->addSql('DROP TABLE rides'); 63 | $this->addSql('DROP TABLE rideEvents'); 64 | $this->addSql('DROP TABLE rideEventTypes'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/AppBundle/Ride/RideEventRepositoryTest.php: -------------------------------------------------------------------------------- 1 | savedRide = $this->ride()->getRepoSavedRide(); 27 | } 28 | 29 | public function testNonExistentRideThrowsException() 30 | { 31 | $nonExistentRide = new Ride( 32 | $this->user()->getSavedUser(), 33 | $this->location()->getSavedHomeLocation() 34 | ); 35 | $this->verifyExceptionWithMessage( 36 | RideNotFoundException::class, 37 | RideNotFoundException::MESSAGE 38 | ); 39 | $this->ride()->getRepoLastEvent($nonExistentRide); 40 | } 41 | 42 | /** 43 | * @throws DuplicateRoleAssignmentException 44 | * @throws UserNotFoundException 45 | * @throws UnauthorizedOperationException 46 | */ 47 | public function testSaveNewRideEvent() 48 | { 49 | $rideEvent = $this->getSavedRequestedRideEvent(); 50 | 51 | self::assertGreaterThan(0, $rideEvent->getId()); 52 | } 53 | 54 | /** 55 | * @throws DuplicateRoleAssignmentException 56 | * @throws UserNotFoundException 57 | * @throws UnauthorizedOperationException 58 | */ 59 | public function testRideIsCurrentlyRequested() 60 | { 61 | $this->getSavedRequestedRideEvent(); 62 | 63 | $lastEventForRide = $this->ride()->getRepoLastEvent($this->savedRide); 64 | 65 | self::assertTrue($lastEventForRide->is(RideEventType::requested())); 66 | } 67 | 68 | /** 69 | * @throws DuplicateRoleAssignmentException 70 | * @throws UserNotFoundException 71 | * @throws UnauthorizedOperationException 72 | */ 73 | public function testRideIsCurrentlyAccepted() 74 | { 75 | $this->assertLastEventIsOfType($this->ride()->accepted); 76 | } 77 | 78 | /** 79 | * @throws DuplicateRoleAssignmentException 80 | * @throws UserNotFoundException 81 | * @throws UnauthorizedOperationException 82 | */ 83 | public function testRideIsCurrentlyInProgress() 84 | { 85 | $this->assertLastEventIsOfType($this->ride()->inProgress); 86 | } 87 | 88 | /** 89 | * @throws DuplicateRoleAssignmentException 90 | * @throws UserNotFoundException 91 | * @throws UnauthorizedOperationException 92 | */ 93 | public function testRideIsCurrentlyCancelled() 94 | { 95 | $this->assertLastEventIsOfType($this->ride()->cancelled); 96 | } 97 | 98 | /** 99 | * @throws DuplicateRoleAssignmentException 100 | * @throws UserNotFoundException 101 | * @throws UnauthorizedOperationException 102 | */ 103 | public function testRideIsCurrentlyCompleted() 104 | { 105 | $this->assertLastEventIsOfType($this->ride()->completed); 106 | } 107 | 108 | /** 109 | * @throws DuplicateRoleAssignmentException 110 | * @throws UserNotFoundException 111 | * @throws UnauthorizedOperationException 112 | */ 113 | public function testRideIsCurrentlyRejected() 114 | { 115 | $this->assertLastEventIsOfType($this->ride()->rejected); 116 | } 117 | 118 | /** 119 | * @throws DuplicateRoleAssignmentException 120 | * @throws UserNotFoundException 121 | * @throws UnauthorizedOperationException 122 | */ 123 | public function testMarkRideAsStatus() 124 | { 125 | $this->ride()->markRepoRide( 126 | $this->savedRide, 127 | $this->user()->getSavedPassenger(), 128 | RideEventType::requested() 129 | ); 130 | $lastEventForRide = $this->ride()->getRepoLastEvent($this->savedRide); 131 | 132 | self::assertTrue($lastEventForRide->is(RideEventType::requested())); 133 | } 134 | 135 | /** 136 | * @return RideEvent 137 | * @throws DuplicateRoleAssignmentException 138 | * @throws UserNotFoundException 139 | * @throws UnauthorizedOperationException 140 | */ 141 | private function getSavedRequestedRideEvent() 142 | { 143 | return $this->ride()->markRepoRide( 144 | $this->savedRide, 145 | $this->user()->getSavedPassenger(), 146 | $this->ride()->requested 147 | ); 148 | } 149 | 150 | /** 151 | * @param RideEventType $eventTypeToAssert 152 | * @return mixed 153 | * @throws DuplicateRoleAssignmentException 154 | * @throws UserNotFoundException 155 | * @throws UnauthorizedOperationException 156 | */ 157 | private function assertLastEventIsOfType(RideEventType $eventTypeToAssert) 158 | { 159 | $this->getSavedRequestedRideEvent(); 160 | 161 | $this->save(new RideEvent( 162 | $this->savedRide, 163 | $this->user()->getSavedUserWithName('Jamie', 'Isaacs'), 164 | $eventTypeToAssert 165 | )); 166 | 167 | $lastEventForRide = $this->ride()->getRepoLastEvent($this->savedRide); 168 | 169 | self::assertFalse($lastEventForRide->is(RideEventType::requested())); 170 | self::assertTrue($lastEventForRide->is($eventTypeToAssert)); 171 | 172 | return $lastEventForRide; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /tests/AppBundle/Production/UserApi.php: -------------------------------------------------------------------------------- 1 | userRepository = new UserRepository( 37 | $entityManager, 38 | $userManager 39 | ); 40 | $this->userService = new UserService( 41 | $this->userRepository 42 | ); 43 | $this->entityManager = $entityManager; 44 | } 45 | 46 | const CLIENT_ID = '1_3bcbxd9e24g0gk4swg0kwgcwg4o8k8g4g888kwc44gcc0gwwk4'; 47 | const CLIENT_SECRET = '4ok2x70rlfokc8g0wws8c8kwcokw80k44sg48goc0ok4w0so0k'; 48 | 49 | /** 50 | * @return UserRepositoryInterface 51 | */ 52 | public function getRepo() 53 | { 54 | return $this->userRepository; 55 | } 56 | 57 | /** 58 | * @param $userId 59 | * @return AppUser 60 | * @throws UserNotFoundException 61 | * @throws UnauthorizedOperationException 62 | */ 63 | public function getServiceUserById($userId) 64 | { 65 | $this->authById($userId); 66 | return $this->userService->getUserById($userId); 67 | } 68 | 69 | public function getSavedUserWithName($first, $last) 70 | { 71 | $fakeUser = new FakeUser($first, $last); 72 | return $this->userService->newUser( 73 | $first, 74 | $last, 75 | $fakeUser->email, 76 | $fakeUser->username, 77 | $fakeUser->password 78 | ); 79 | } 80 | 81 | /** 82 | * @return AppUser 83 | */ 84 | public function getSavedUser() 85 | { 86 | return $this->getSavedUserWithName('chris', 'holland'); 87 | } 88 | 89 | /** 90 | * @return AppUser 91 | * @throws DuplicateRoleAssignmentException 92 | * @throws UnauthorizedOperationException 93 | */ 94 | public function getNewPassenger() 95 | { 96 | $passenger = $this->getSavedUser(); 97 | $this->makeUserPassenger($this->getSavedUser()); 98 | return $passenger; 99 | } 100 | 101 | /** 102 | * @param AppUser $user 103 | * @throws DuplicateRoleAssignmentException 104 | * @throws UnauthorizedOperationException 105 | */ 106 | public function makeUserPassenger(AppUser $user) 107 | { 108 | $this->auth($user); 109 | $this->userService->makeUserPassenger($user); 110 | } 111 | 112 | /** 113 | * @return AppUser 114 | * @throws DuplicateRoleAssignmentException 115 | * @throws UserNotFoundException 116 | * @throws UnauthorizedOperationException 117 | */ 118 | public function getSavedPassenger(): AppUser 119 | { 120 | $ridePassenger = $this->getNewPassenger(); 121 | $savedPassenger = $this->getServiceUserById($ridePassenger->getId()); 122 | 123 | return $savedPassenger; 124 | } 125 | 126 | /** 127 | * @param AppUser $driver 128 | * @throws DuplicateRoleAssignmentException 129 | * @throws UnauthorizedOperationException 130 | */ 131 | public function makeUserDriver(AppUser $driver) 132 | { 133 | $this->auth($driver); 134 | $this->userService->makeUserDriver($driver); 135 | } 136 | 137 | /** 138 | * @return AppUser 139 | * @throws DuplicateRoleAssignmentException 140 | * @throws UnauthorizedOperationException 141 | */ 142 | public function getNewDriver() 143 | { 144 | return $this->getNewDriverWithName('new', 'driver'); 145 | } 146 | 147 | /** 148 | * @param $first 149 | * @param $last 150 | * @return AppUser 151 | * @throws DuplicateRoleAssignmentException 152 | * @throws UnauthorizedOperationException 153 | */ 154 | public function getNewDriverWithName($first, $last) 155 | { 156 | $driver = $this->getSavedUserWithName($first, $last); 157 | $this->makeUserDriver($driver); 158 | return $driver; 159 | } 160 | 161 | private function saveRole(AppRole $role) 162 | { 163 | $this->entityManager->persist($role); 164 | $this->entityManager->flush(); 165 | } 166 | 167 | public function bootStrapRoles() 168 | { 169 | $this->saveRole(AppRole::driver()); 170 | $this->saveRole(AppRole::passenger()); 171 | } 172 | 173 | /** 174 | * @return UserService 175 | */ 176 | public function getService() : UserService 177 | { 178 | return $this->userService; 179 | } 180 | 181 | public function setAuthenticatedUser(AppUser $user) 182 | { 183 | $this->authUserIsSet = true; 184 | $this->userService->setAuthenticatedUser($user); 185 | } 186 | 187 | private function auth(AppUser $user) 188 | { 189 | if (! $this->authUserIsSet) { 190 | $this->userService->setAuthenticatedUser($user); 191 | } 192 | } 193 | 194 | /** 195 | * @param $userId 196 | * @throws UserNotFoundException 197 | */ 198 | public function authById($userId): void 199 | { 200 | if (! $this->authUserIsSet) { 201 | $this->userService->setAuthenticatedUser( 202 | $this->userRepository->getUserById($userId) 203 | ); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /.idea/php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/AppBundle/Service/RideService.php: -------------------------------------------------------------------------------- 1 | rideRepository = $rideRepository; 40 | $this->rideEventRepository = $rideEventRepository; 41 | } 42 | 43 | /** 44 | * @param AppUser $passenger 45 | * @param AppLocation $departure 46 | * @return Ride 47 | * @throws UserNotInPassengerRoleException 48 | */ 49 | public function newRide(AppUser $passenger, AppLocation $departure): Ride 50 | { 51 | $this->validateUserHasPassengerRole($passenger); 52 | 53 | $newRide = new Ride($passenger, $departure); 54 | $this->rideRepository->saveRide($newRide); 55 | 56 | $this->rideEventRepository->markRideStatusByPassenger( 57 | $newRide, 58 | RideEventType::requested() 59 | ); 60 | 61 | return $newRide; 62 | } 63 | 64 | /** 65 | * @param Uuid $id 66 | * @return Ride 67 | * @throws RideNotFoundException 68 | */ 69 | public function getRide(Uuid $id): Ride 70 | { 71 | return $this->rideRepository->getRideById($id); 72 | } 73 | 74 | public function assignDestinationToRide(Ride $ride, AppLocation $destination) 75 | { 76 | $this->rideRepository->assignDestinationToRide( 77 | $ride, 78 | $destination 79 | ); 80 | return $ride; 81 | } 82 | 83 | /** 84 | * @param Ride $ride 85 | * @return RideEventType 86 | * @throws RideNotFoundException 87 | */ 88 | public function getRideStatus(Ride $ride): RideEventType 89 | { 90 | return 91 | $this->rideEventRepository 92 | ->getLastEventForRide($ride) 93 | ->getStatus(); 94 | } 95 | 96 | /** 97 | * @param Ride $requestedRide 98 | * @param AppUser $driver 99 | * @return Ride 100 | * @throws RideLifeCycleException 101 | * @throws RideNotFoundException 102 | * @throws UserNotInDriverRoleException 103 | */ 104 | public function acceptRide(Ride $requestedRide, AppUser $driver): Ride 105 | { 106 | $this->validateRideIsRequested($requestedRide); 107 | 108 | $this->validateUserHasDriverRole($driver); 109 | $this->markRide( 110 | $requestedRide, 111 | $driver, 112 | RideEventType::accepted() 113 | ); 114 | 115 | $this->rideRepository->assignDriverToRide( 116 | $requestedRide, 117 | $driver 118 | ); 119 | 120 | return $requestedRide; 121 | } 122 | 123 | /** 124 | * @param Ride $acceptedRide 125 | * @param AppUser $driver 126 | * @return Ride 127 | * @throws RideLifeCycleException 128 | * @throws UserNotInDriverRoleException 129 | * @throws RideNotFoundException 130 | * @throws ActingDriverIsNotAssignedDriverException 131 | */ 132 | public function markRideInProgress(Ride $acceptedRide, AppUser $driver): Ride 133 | { 134 | $this->validateRideIsAccepted($acceptedRide); 135 | 136 | $this->transitionToStatusByAssignedDriver( 137 | $acceptedRide, 138 | $driver, 139 | RideEventType::inProgress() 140 | ); 141 | 142 | return $acceptedRide; 143 | } 144 | 145 | /** 146 | * @param Ride $rideInProgress 147 | * @param AppUser $driver 148 | * @return Ride 149 | * @throws ActingDriverIsNotAssignedDriverException 150 | * @throws RideLifeCycleException 151 | * @throws RideNotFoundException 152 | * @throws UserNotInDriverRoleException 153 | */ 154 | public function markRideCompleted(Ride $rideInProgress, AppUser $driver): Ride 155 | { 156 | $this->validateRideIsInProgress($rideInProgress); 157 | 158 | $this->transitionToStatusByAssignedDriver( 159 | $rideInProgress, 160 | $driver, 161 | RideEventType::completed() 162 | ); 163 | return $rideInProgress; 164 | } 165 | 166 | /** 167 | * @param AppUser $passenger 168 | * @throws UserNotInPassengerRoleException 169 | */ 170 | private function validateUserHasPassengerRole(AppUser $passenger): void 171 | { 172 | if (!$passenger->userHasRole(AppRole::passenger())) { 173 | throw new UserNotInPassengerRoleException(); 174 | } 175 | } 176 | 177 | /** 178 | * @param AppUser $driver 179 | * @throws UserNotInDriverRoleException 180 | */ 181 | private function validateUserHasDriverRole(AppUser $driver): void 182 | { 183 | if (!$driver->userHasRole(AppRole::driver())) { 184 | throw new UserNotInDriverRoleException(); 185 | } 186 | } 187 | 188 | /** 189 | * @param Ride $ride 190 | * @throws RideLifeCycleException 191 | * @throws RideNotFoundException 192 | */ 193 | private function validateRideIsRequested(Ride $ride): void 194 | { 195 | if (!RideEventType::requested()->equals( 196 | $this->getRideStatus($ride) 197 | )) { 198 | throw new RideLifeCycleException(); 199 | } 200 | } 201 | 202 | /** 203 | * @param Ride $acceptedRide 204 | * @throws RideLifeCycleException 205 | * @throws RideNotFoundException 206 | */ 207 | private function validateRideIsAccepted(Ride $acceptedRide): void 208 | { 209 | if (!RideEventType::accepted()->equals( 210 | $this->getRideStatus($acceptedRide) 211 | )) { 212 | throw new RideLifeCycleException(); 213 | } 214 | } 215 | 216 | /** 217 | * @param Ride $acceptedRide 218 | * @param AppUser $driver 219 | * @throws ActingDriverIsNotAssignedDriverException 220 | */ 221 | private function validateAttemptingDriverIsAssignedDriver(Ride $acceptedRide, AppUser $driver): void 222 | { 223 | if (!$acceptedRide->isDrivenBy($driver)) { 224 | throw new ActingDriverIsNotAssignedDriverException(); 225 | } 226 | } 227 | 228 | /** 229 | * @param Ride $rideInProgress 230 | * @throws RideLifeCycleException 231 | * @throws RideNotFoundException 232 | */ 233 | private function validateRideIsInProgress(Ride $rideInProgress): void 234 | { 235 | if (!RideEventType::inProgress()->equals($this->getRideStatus($rideInProgress))) { 236 | throw new RideLifeCycleException(); 237 | } 238 | } 239 | 240 | private function markRide(Ride $ride, AppUser $driver, RideEventType $status): void 241 | { 242 | $this->rideEventRepository->markRideStatusByActor( 243 | $ride, 244 | $driver, 245 | $status 246 | ); 247 | } 248 | 249 | /** 250 | * @param Ride $ride 251 | * @param AppUser $driver 252 | * @param $statusToTransition 253 | * @throws UserNotInDriverRoleException 254 | * @throws ActingDriverIsNotAssignedDriverException 255 | */ 256 | private function transitionToStatusByAssignedDriver(Ride $ride, AppUser $driver, $statusToTransition): void 257 | { 258 | $this->validateUserHasDriverRole($driver); 259 | $this->validateAttemptingDriverIsAssignedDriver($ride, $driver); 260 | $this->markRide( 261 | $ride, 262 | $driver, 263 | $statusToTransition 264 | ); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /.idea/kata_tdd_php_symfony.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | --------------------------------------------------------------------------------