├── .gitignore ├── Common ├── composer.json ├── composer.lock ├── run.sh ├── src │ ├── Di │ │ ├── Alias │ │ │ ├── DatabaseIdentifierGenerator.php │ │ │ ├── IncomingMessageStore.php │ │ │ └── OutgoingMessageStore.php │ │ └── Factory │ │ │ ├── AbstractMessageListenerFactory.php │ │ │ ├── AmqpMessagePublisherFactory.php │ │ │ ├── AmqpMessageSubscriberFactory.php │ │ │ ├── AuthorizationMiddlewareFactory.php │ │ │ ├── ClientMongoDbFactory.php │ │ │ ├── DatabaseMongoFactory.php │ │ │ ├── ErrorHandlerMiddlewareFactory.php │ │ │ ├── ErrorMessageTimeoutTrackerMongoDbFactory.php │ │ │ ├── IdentifierGeneratorAutoIncrementFactory.php │ │ │ ├── IncomingMessageStoreMongoDbFactory.php │ │ │ ├── LoggerInterfaceStdoutFactory.php │ │ │ ├── MessageDeliveryServiceFactory.php │ │ │ ├── MessageRouterFactory.php │ │ │ ├── MonologFileLoggerHandlerFactory.php │ │ │ ├── MonologLoggerFactory.php │ │ │ ├── OutgoingMessageStoreMongoDbFactory.php │ │ │ ├── PublishedMessageTrackerMongoDbFactory.php │ │ │ └── RestFullMiddlewareAbstractFactory.php │ └── Ui │ │ ├── Http │ │ └── Restful │ │ │ ├── Authorization │ │ │ ├── AuthorizationRulesDefinitionException.php │ │ │ ├── AuthorizationService.php │ │ │ ├── AuthorizationType.php │ │ │ ├── JwtToken │ │ │ │ ├── JwtTokenFactory.php │ │ │ │ ├── JwtTokenParser.php │ │ │ │ └── JwtTokenValidator.php │ │ │ ├── Token.php │ │ │ ├── TokenFactory.php │ │ │ ├── TokenParser.php │ │ │ └── TokenValidator.php │ │ │ └── Middleware │ │ │ ├── AbstractRestfulResourceMiddleware.php │ │ │ ├── AuthorizationMiddleware.php │ │ │ └── ErrorHandlerMiddleware.php │ │ └── Messaging │ │ └── Listener │ │ └── AbstractMessageListener.php └── tests │ └── Ui │ └── Http │ └── Restful │ ├── Authorization │ └── JwtToken │ │ ├── JwtTokenFactoryTest.php │ │ ├── JwtTokenParserTest.php │ │ └── JwtTokenValidatorTest.php │ └── Middleware │ └── AuthorizationMiddlewareTest.php ├── MergePullRequestPm ├── bin │ ├── listen-messages │ └── publish-messages ├── composer.json ├── composer.lock ├── run.sh ├── src │ ├── Application │ │ └── MergePullRequestEventBus.php │ ├── Domain │ │ ├── Command │ │ │ ├── CollectMoneyCommand.php │ │ │ ├── MergePullRequestCommand.php │ │ │ ├── PayMoneyCommand.php │ │ │ └── RecipientAddress.php │ │ ├── MergePullRequestProcess.php │ │ ├── MergePullRequestState.php │ │ ├── Provider │ │ │ ├── PricingProvider.php │ │ │ ├── PullRequest.php │ │ │ └── PullRequestProvider.php │ │ └── UseCase │ │ │ ├── MoneyCollected.php │ │ │ ├── MoneyCollectedHandler.php │ │ │ ├── MoneyPayed.php │ │ │ ├── MoneyPayedHandler.php │ │ │ ├── PullRequestMarkedAsMergeable.php │ │ │ ├── PullRequestMarkedAsMergeableHandler.php │ │ │ ├── PullRequestMerged.php │ │ │ └── PullRequestMergedHandler.php │ └── Infrastructure │ │ ├── Di │ │ └── ZendServiceManager │ │ │ ├── Alias │ │ │ └── MergePullRequestPmProjectionTable.php │ │ │ ├── Factory │ │ │ ├── AbstractMessageListenerFactory.php │ │ │ ├── MoneyCollectedHandlerFactory.php │ │ │ ├── MoneyPayedHandlerFactory.php │ │ │ ├── PullRequestMarkedAsMergeableHandlerFactory.php │ │ │ ├── PullRequestProviderMongoDbFactory.php │ │ │ └── RepositoryMongoDbFactory.php │ │ │ ├── autoload │ │ │ ├── config.global.php │ │ │ └── config.local.php │ │ │ ├── config.php │ │ │ └── container.php │ │ ├── Persistence │ │ ├── InMemory │ │ │ ├── PricingProviderInMemory.php │ │ │ └── PullRequestProviderInMemory.php │ │ └── MongoDb │ │ │ ├── PullRequestProviderMongoDb.php │ │ │ └── RepositoryMongoDb.php │ │ └── Ui │ │ └── Messaging │ │ ├── Listener │ │ ├── AbstractMessageListener.php │ │ ├── MoneyCollectedListener.php │ │ ├── MoneyPayedListener.php │ │ ├── PullRequestMarkedAsMergeableListener.php │ │ └── PullRequestMergedListener.php │ │ └── routes.php ├── tests │ └── UseCase │ │ ├── MoneyCollectedHandlerTest.php │ │ ├── MoneyPayedHandlerTest.php │ │ └── PullRequestMergedHandlerTest.php └── var │ └── .gitkeep ├── MessagePublisher ├── bin │ └── publish-messages ├── composer.json ├── composer.lock ├── run.sh └── src │ └── Infrastructure │ └── Di │ └── ZendServiceManager │ ├── Factory │ └── AmqpMessagePublisherFactory.php │ ├── autoload │ ├── config.global.php │ └── config.local.php │ ├── config.php │ └── container.php ├── Payment ├── bin │ ├── listen-messages │ └── publish-messages ├── composer.json ├── composer.lock ├── run.sh ├── src │ ├── Application │ │ ├── PaymentCommandBus.php │ │ └── Projection │ │ │ └── PaymentProjector.php │ ├── Domain │ │ ├── Event │ │ │ ├── CollectMoneyFailed.php │ │ │ ├── MoneyCollected.php │ │ │ ├── MoneyPayed.php │ │ │ └── PayMoneyFailed.php │ │ ├── PaymentProvider.php │ │ ├── Transaction.php │ │ └── UseCase │ │ │ ├── CollectMoneyCommand.php │ │ │ ├── CollectMoneyCommandHandler.php │ │ │ ├── PayMoneyCommand.php │ │ │ └── PayMoneyCommandHandler.php │ └── Infrastructure │ │ ├── Di │ │ └── ZendServiceManager │ │ │ ├── Alias │ │ │ ├── PaymentProjectionTable.php │ │ │ └── PaymentRepository.php │ │ │ ├── Factory │ │ │ ├── PaymentProjectionTableMongoDbFactory.php │ │ │ └── PaymentRepositoryMongoDbFactory.php │ │ │ ├── autoload │ │ │ ├── config.global.php │ │ │ └── config.local.php │ │ │ ├── config.php │ │ │ └── container.php │ │ ├── Domain │ │ └── PaymentProviderFake.php │ │ ├── Persistence │ │ └── MongoDb │ │ │ ├── ProjectionTableMongoDb.php │ │ │ └── RepositoryMongoDb.php │ │ └── Ui │ │ ├── Http │ │ ├── Restful │ │ │ ├── Resource │ │ │ │ ├── AbstractRestfulResourceMiddleware.php │ │ │ │ ├── CollectMoneyResource.php │ │ │ │ └── PayoutMoneyResource.php │ │ │ └── routes.php │ │ ├── index.php │ │ └── pipeline.php │ │ └── Messaging │ │ ├── Listener │ │ ├── CollectMoneyCommandListener.php │ │ └── PayMoneyCommandListener.php │ │ └── routes.php └── tests │ └── Payment │ └── UseCase │ ├── CollectMoneyTest.php │ └── PayMoneyTest.php ├── PullRequest ├── bin │ ├── listen-messages │ └── publish-messages ├── composer.json ├── composer.lock ├── run.sh ├── src │ ├── Application │ │ ├── Projection │ │ │ └── PullRequestProjector.php │ │ └── PullRequestCommandBus.php │ ├── Domain │ │ ├── Event │ │ │ ├── ApprovePullRequestFailed.php │ │ │ ├── MergePullRequestFailed.php │ │ │ ├── PullRequestApproved.php │ │ │ ├── PullRequestCreated.php │ │ │ ├── PullRequestCreationFailed.php │ │ │ ├── PullRequestMarkedAsMergeable.php │ │ │ ├── PullRequestMerged.php │ │ │ ├── PullRequestReviewerAssignationFailed.php │ │ │ └── PullRequestReviewerAssigned.php │ │ ├── PullRequest.php │ │ └── UseCase │ │ │ ├── ApprovePullRequestCommand.php │ │ │ ├── ApprovePullRequestCommandHandler.php │ │ │ ├── AssignPullRequestReviewerCommand.php │ │ │ ├── AssignPullRequestReviewerCommandHandler.php │ │ │ ├── CreatePullRequestCommand.php │ │ │ ├── CreatePullRequestCommandHandler.php │ │ │ ├── MergePullRequestCommand.php │ │ │ └── MergePullRequestCommandHandler.php │ └── Infrastructure │ │ ├── Di │ │ └── ZendServiceManager │ │ │ ├── Alias │ │ │ ├── PullRequestProjectionTable.php │ │ │ └── PullRequestRepository.php │ │ │ ├── Factory │ │ │ ├── PullRequestProjectionTableMongoDbFactory.php │ │ │ └── PullRequestRepositoryMongoDbFactory.php │ │ │ ├── autoload │ │ │ ├── config.global.php │ │ │ └── config.local.php │ │ │ ├── config.php │ │ │ └── container.php │ │ ├── Persistence │ │ └── MongoDb │ │ │ ├── ProjectionTableMongoDb.php │ │ │ └── RepositoryMongoDb.php │ │ └── Ui │ │ ├── Http │ │ ├── Restful │ │ │ ├── AuthorizationRules.php │ │ │ ├── Resource │ │ │ │ ├── PullRequestApproveResource.php │ │ │ │ ├── PullRequestCollectionResource.php │ │ │ │ └── PullRequestReviewerResource.php │ │ │ └── routes.php │ │ ├── index.php │ │ └── pipeline.php │ │ └── Messaging │ │ ├── Listener │ │ └── MergePullRequestCommandListener.php │ │ └── routes.php ├── tests │ └── PullRequest │ │ └── UseCase │ │ ├── ApprovePullRequestTest.php │ │ ├── AssignReviewerTest.php │ │ ├── CreatePullRequestTest.php │ │ └── MergePullRequestTest.php └── var │ └── .gitkeep ├── README.md ├── UserIdentity ├── composer.json ├── composer.lock ├── run.sh ├── src │ ├── Application │ │ ├── Projection │ │ │ └── UserProjector.php │ │ └── UserIdentityCommandBus.php │ ├── Domain │ │ ├── AllowedRoles.php │ │ ├── ContentValidationResult.php │ │ ├── Event │ │ │ ├── LogUserInWithPasswordFailed.php │ │ │ ├── RefreshUserAccessTokenFailed.php │ │ │ ├── UserAccessTokenRefreshed.php │ │ │ └── UserWithPasswordLoggedIn.php │ │ ├── PasswordEncryption.php │ │ ├── UseCase │ │ │ ├── LogUserInWithPasswordCommand.php │ │ │ ├── LogUserInWithPasswordCommandHandler.php │ │ │ ├── RefreshUserAccessTokenCommand.php │ │ │ └── RefreshUserAccessTokenCommandHandler.php │ │ ├── User.php │ │ └── UserWithPasswordContentValidator.php │ └── Infrastructure │ │ ├── Di │ │ └── ZendServiceManager │ │ │ ├── Alias │ │ │ ├── UserProjectionTable.php │ │ │ └── UserRepository.php │ │ │ ├── Factory │ │ │ ├── JwtTokenFactoryFactory.php │ │ │ ├── JwtTokenValidatorFactory.php │ │ │ ├── UseCase │ │ │ │ ├── RefreshUserAccessTokenCommandHandlerFactory.php │ │ │ │ └── RegisterUserWithPasswordCommandHandlerFactory.php │ │ │ ├── UserProjectionTableMongoDbFactory.php │ │ │ └── UserRepositoryMongoDbFactory.php │ │ │ ├── autoload │ │ │ ├── config.global.php │ │ │ └── config.local.php │ │ │ ├── config.php │ │ │ └── container.php │ │ ├── Domain │ │ └── BCryptPasswordEncryption.php │ │ ├── Persistence │ │ └── MongoDb │ │ │ ├── ProjectionTableMongoDb.php │ │ │ └── RepositoryMongoDb.php │ │ └── Ui │ │ └── Http │ │ ├── Restful │ │ ├── AuthorizationRules.php │ │ ├── Resource │ │ │ ├── UserAccessTokenResource.php │ │ │ └── UserWithPasswordCollectionResource.php │ │ └── routes.php │ │ ├── index.php │ │ └── pipeline.php └── tests │ └── UserIdentity │ ├── Double │ ├── PasswordEncryptionStub.php │ ├── TokenFactoryStub.php │ ├── TokenValidatorStub.php │ └── UserWithPasswordContentValidatorStub.php │ └── UseCase │ ├── LogUserInWithPasswordCommandHandlerTest.php │ └── RefreshUserAccessTokenCommandHandlerTest.php ├── code_review_restlet.json ├── docker ├── .php_cs.dist ├── Dockerfile ├── Dockerfile.nginx ├── default.conf ├── gateway-docker-compose.yml ├── infra-docker-compose.yml ├── lint_code.sh ├── nginx.conf ├── nginx.tmpl ├── services-docker-compose.yml └── wait-for-it.sh └── install.sh /.gitignore: -------------------------------------------------------------------------------- 1 | */vendor 2 | */bin 3 | .idea 4 | file.log -------------------------------------------------------------------------------- /Common/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mgonzalezbaile/common", 3 | "description": "Common library for Code Review app", 4 | "license": "MIT", 5 | "require": { 6 | "php": "^7.2", 7 | "psr/container": "^1.0", 8 | "lcobucci/jwt": "^3.2", 9 | "psr/http-server-middleware": "^1.0", 10 | "soa-php/message-store-amqp": "^1.0", 11 | "soa-php/message-store-mongodb": "^1.0", 12 | "soa-php/event-sourcing-middleware": "^1.0", 13 | "zendframework/zend-servicemanager": "^3.3", 14 | "zendframework/zend-config-aggregator": "^1.1", 15 | "zendframework/zend-component-installer": "^2.1.1", 16 | "zendframework/zend-diactoros": "^1.7.1 || ^2.0", 17 | "zendframework/zend-expressive": "^3.0.1", 18 | "zendframework/zend-expressive-helpers": "^5.0", 19 | "zendframework/zend-stdlib": "^3.1", 20 | "zendframework/zend-expressive-fastroute": "^3.0", 21 | "zendframework/zend-problem-details": "^1.0", 22 | "martinezdelariva/hydrator": "^1.0", 23 | "martinezdelariva/functional": "dev-master", 24 | "ext-json": "*", 25 | "ext-mongodb": "*", 26 | "filp/whoops": "^2.3", 27 | "beberlei/assert": "^3.2", 28 | "lukasoppermann/http-status": "^2.0", 29 | "symfony/var-dumper": "^4.2", 30 | "monolog/monolog": "^1.24" 31 | }, 32 | "require-dev": { 33 | "friendsofphp/php-cs-fixer": "^2.12", 34 | "phpunit/phpunit": "^7.4" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Common\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "CommonTest\\": "tests/" 44 | } 45 | }, 46 | "config": { 47 | "bin-dir": "bin/" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Common/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | COMMAND=${1:-} 4 | shift 5 | 6 | run_in_docker="docker run -it --rm -v $PWD/..:/srv/app -w /srv/app/Common --user ${UID} mgonzalezbaile/php_base:1.0" 7 | 8 | case "$COMMAND" in 9 | composer) 10 | ${run_in_docker} composer $@ 11 | ;; 12 | phpunit) 13 | ${run_in_docker} vendor/phpunit/phpunit/phpunit tests 14 | ;; 15 | esac 16 | -------------------------------------------------------------------------------- /Common/src/Di/Alias/DatabaseIdentifierGenerator.php: -------------------------------------------------------------------------------- 1 | get('config')['service-name']; 16 | 17 | return new AmqpMessagePublisher(new AmqpPublisherConfig($serviceName, $container->get('config')['rabbitmq']['credentials'], [$serviceName])); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/AmqpMessageSubscriberFactory.php: -------------------------------------------------------------------------------- 1 | get(MessageRouter::class), 21 | new AmqpSubscriberConfig($container->get('config')['rabbitmq']['credentials'], $container->get('config')['service-name']), 22 | new AmqpErrorMessageHandler( 23 | $container->get(ErrorMessageTimeoutTracker::class), 24 | new ClockImpl(), 25 | $container->get('config')['rabbitmq']['dead-letter-seconds'] 26 | ) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/AuthorizationMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | get('config')['authorization-rules']), $container->get(TokenParser::class)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/ClientMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get('config')['mongo-db']['connection']); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/DatabaseMongoFactory.php: -------------------------------------------------------------------------------- 1 | get(Client::class); 16 | 17 | return $client 18 | ->selectDatabase($container->get('config')['mongo-db']['database']) 19 | ->withOptions(['typeMap' =>['document' => 'array', 'root' => 'array']]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/ErrorHandlerMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | get(LoggerInterface::class), $container->get(ProblemDetailsResponseFactory::class)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/ErrorMessageTimeoutTrackerMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 18 | $collection = $database->selectCollection('error_messages'); 19 | 20 | return new ErrorMessageTimeoutTrackerMongoDb($collection, new ClockImpl()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/IdentifierGeneratorAutoIncrementFactory.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 18 | $identifiersCollection = $database->selectCollection('identifiers'); 19 | 20 | return new IdentifierGeneratorAutoIncrementMongoDb($identifiersCollection); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/IncomingMessageStoreMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 19 | $messagesCollection = $database->selectCollection('incoming_messages'); 20 | $identifierGenerator = $container->get(DatabaseIdentifierGenerator::class); 21 | 22 | return new MessageStoreMongoDb($messagesCollection, $identifierGenerator); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/LoggerInterfaceStdoutFactory.php: -------------------------------------------------------------------------------- 1 | slice_array_depth($context, 2), true) . "\n"; 28 | } 29 | } 30 | 31 | private function slice_array_depth($array, $depth = 0) 32 | { 33 | foreach ($array as $key => $value) { 34 | if (is_array($value)) { 35 | if ($depth > 0) { 36 | $array[$key] = $this->slice_array_depth($value, $depth - 1); 37 | } else { 38 | unset($array[$key]); 39 | } 40 | } 41 | } 42 | 43 | return $array; 44 | } 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/MessageDeliveryServiceFactory.php: -------------------------------------------------------------------------------- 1 | get(OutgoingMessageStore::class), 19 | $container->get(PublishedMessageTracker::class) 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/MessageRouterFactory.php: -------------------------------------------------------------------------------- 1 | get('config')['logger-handlers']['file']; 15 | $formatter = $config['formatter']; 16 | $logPath = $config['path']; 17 | $handler = new StreamHandler($logPath, $config['level']); 18 | 19 | $handler->setFormatter(new $formatter()); 20 | 21 | return $handler; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/MonologLoggerFactory.php: -------------------------------------------------------------------------------- 1 | get('config')['enabled-loggers']); 15 | $serviceName = $container->get('config')['service-name']; 16 | 17 | $logger = new Logger($serviceName); 18 | 19 | foreach ($enabledLoggerHandlers as $loggerHandler) { 20 | $loggerHandlerFactory = new $loggerHandler(); 21 | $logger->pushHandler($loggerHandlerFactory($container)); 22 | } 23 | 24 | return $logger; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/OutgoingMessageStoreMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 18 | $messagesCollection = $database->selectCollection('outgoing_messages'); 19 | $identifierGenerator = $container->get(DatabaseIdentifierGenerator::class); 20 | 21 | return new MessageStoreMongoDb($messagesCollection, $identifierGenerator); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/PublishedMessageTrackerMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 17 | $trackerCollection = $database->selectCollection('messages_tracker'); 18 | 19 | return new PublishedMessageTrackerMongoDb($trackerCollection); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Common/src/Di/Factory/RestFullMiddlewareAbstractFactory.php: -------------------------------------------------------------------------------- 1 | authorizationRules = $authorizationRules; 17 | } 18 | 19 | public function isAuthDefinedForRoute(string $uri, string $method): bool 20 | { 21 | if (!isset($this->authorizationRules[$uri])) { 22 | return false; 23 | } 24 | 25 | if (!isset($this->authorizationRules[$uri][$method])) { 26 | return false; 27 | } 28 | 29 | return true; 30 | } 31 | 32 | /** 33 | * @throws AuthorizationRulesDefinitionException 34 | */ 35 | public function isAuthRequiredForRoute(string $uri, string $method): bool 36 | { 37 | if (!$this->isAuthDefinedForRoute($uri, $method)) { 38 | throw AuthorizationRulesDefinitionException::withRoute($uri, $method); 39 | } 40 | 41 | if (AuthorizationType::NO_AUTH === $this->authorizationRules[$uri][$method]) { 42 | return false; 43 | } 44 | 45 | return true; 46 | } 47 | 48 | public function isUserAuthorizedForRoute(array $userRoles, string $uri, string $method): bool 49 | { 50 | if (!$this->isAuthRequiredForRoute($uri, $method)) { 51 | return true; 52 | } 53 | 54 | return array_reduce( 55 | $userRoles, 56 | function (bool $isAlreadyAuthorized, string $userRole) use ($uri, $method) { 57 | if ($isAlreadyAuthorized) { 58 | return true; 59 | } 60 | 61 | return in_array($userRole, $this->authorizationRules[$uri][$method]); 62 | }, 63 | false 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Common/src/Ui/Http/Restful/Authorization/AuthorizationType.php: -------------------------------------------------------------------------------- 1 | parse($token); 16 | 17 | return new Token( 18 | $token->getClaim(Token::USER_ID_CLAIM), 19 | $token->getClaim(Token::USER_ROLES_CLAIM), 20 | $token->getClaim(Token::TOKEN_TYPE_CLAIM) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Common/src/Ui/Http/Restful/Authorization/JwtToken/JwtTokenValidator.php: -------------------------------------------------------------------------------- 1 | configuration = $configuration; 27 | $this->signer = $signer; 28 | } 29 | 30 | public function isValid(string $token): bool 31 | { 32 | $data = new ValidationData(); 33 | $token = (new Parser())->parse($token); 34 | 35 | return $token->validate($data) && $token->verify($this->signer, $this->configuration[JwtTokenFactory::KEY]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Common/src/Ui/Http/Restful/Authorization/Token.php: -------------------------------------------------------------------------------- 1 | userId = $userId; 33 | $this->roles = $roles; 34 | $this->type = $type; 35 | } 36 | 37 | public function userId(): string 38 | { 39 | return $this->userId; 40 | } 41 | 42 | public function roles(): array 43 | { 44 | return $this->roles; 45 | } 46 | 47 | public function type(): string 48 | { 49 | return $this->type; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Common/src/Ui/Http/Restful/Authorization/TokenFactory.php: -------------------------------------------------------------------------------- 1 | authorizationService = $authorizationService; 33 | $this->tokenParser = $tokenParser; 34 | } 35 | 36 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 37 | { 38 | $route = $request->getAttribute(RouteResult::class, false); 39 | if (($route instanceof RouteResult) && $route->getMatchedRoute()) { 40 | $routeName = $route->getMatchedRoute()->getName(); 41 | 42 | if (!$this->authorizationService->isAuthRequiredForRoute($routeName, $request->getMethod())) { 43 | return $handler->handle($request); 44 | } 45 | 46 | $token = str_replace('Bearer ', '', $request->getHeaderLine('Authorization')); 47 | 48 | if (empty($token)) { 49 | return new EmptyResponse(Httpstatuscodes::HTTP_UNAUTHORIZED); 50 | } 51 | 52 | $token = $this->tokenParser->parse($token); 53 | 54 | if (!$this->authorizationService->isUserAuthorizedForRoute($token->roles(), $routeName, $request->getMethod())) { 55 | return new EmptyResponse(Httpstatuscodes::HTTP_UNAUTHORIZED); 56 | } 57 | 58 | $request = $request->withAttribute(Token::class, $token); 59 | } 60 | 61 | return $handler->handle($request); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Common/src/Ui/Http/Restful/Middleware/ErrorHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 29 | $this->problemDetailsResponseFactory = $problemDetailsResponseFactory; 30 | } 31 | 32 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 33 | { 34 | set_error_handler($this->createErrorHandler()); 35 | 36 | try { 37 | $response = $handler->handle($request); 38 | } catch (\Throwable $e) { 39 | $this->logger->error($e->getMessage(), ['exception' => $e]); 40 | 41 | return $this->problemDetailsResponseFactory->createResponseFromThrowable($request, $e); 42 | } 43 | 44 | restore_error_handler(); 45 | 46 | return $response; 47 | } 48 | 49 | private function createErrorHandler(): callable 50 | { 51 | return function (int $errno, string $errstr, string $errfile, int $errline): void { 52 | if (!(error_reporting() & $errno)) { 53 | // error_reporting does not include this error 54 | return; 55 | } 56 | 57 | throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Common/src/Ui/Messaging/Listener/AbstractMessageListener.php: -------------------------------------------------------------------------------- 1 | container = $container; 23 | } 24 | 25 | public function commandBus(string $commandBusFqcn, Message $message): CommandBus 26 | { 27 | $trace = new Trace( 28 | $message->id(), 29 | \DateTimeImmutable::createFromFormat('U.u', (string) microtime(true))->format('Y-m-d H:i:s.u'), 30 | $message->correlationId(), 31 | $message->causationId(), 32 | $message->replyTo(), 33 | $message->processId() 34 | ); 35 | 36 | return new $commandBusFqcn($this->container, $trace); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Common/tests/Ui/Http/Restful/Authorization/JwtToken/JwtTokenParserTest.php: -------------------------------------------------------------------------------- 1 | set(Token::USER_ID_CLAIM, $userId) 24 | ->set(Token::USER_ROLES_CLAIM, $userRoles) 25 | ->getToken(); 26 | 27 | $jwtToken = (new Parser())->parse($token); 28 | 29 | $this->assertEquals($userId, $jwtToken->getClaim(Token::USER_ID_CLAIM)); 30 | $this->assertEquals($userRoles, $jwtToken->getClaim(Token::USER_ROLES_CLAIM)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MergePullRequestPm/bin/listen-messages: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 | get(MessageRouter::class), $container->get(MessageSubscriber::class), new PcntlLoopFactory()); 25 | 26 | (require __DIR__ . '/../src/Infrastructure/Ui/Messaging/routes.php')($app, $container); 27 | 28 | try { 29 | $app->startConsuming(); 30 | } catch (\Throwable $exception) { 31 | /** @var LoggerInterface $logger */ 32 | $logger = $container->get(LoggerInterface::class); 33 | $logger->error( 34 | $exception->getMessage(), 35 | [ 36 | 'exception' => $exception, 37 | 'message' => $exception->getMessage(), 38 | ] 39 | ); 40 | } 41 | })(); 42 | -------------------------------------------------------------------------------- /MergePullRequestPm/bin/publish-messages: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 | get(LoggerInterface::class); 25 | $app = new PublisherApplication($container->get(MessagePublisher::class),$container->get(MessageDeliveryService::class),$logger, new PcntlLoopFactory()); 26 | 27 | try { 28 | $app->startPublishing(); 29 | } catch (\Throwable $exception) { 30 | /** @var LoggerInterface $logger */ 31 | $logger->error( 32 | $exception->getMessage(), 33 | [ 34 | 'exception' => $exception, 35 | 'message' => $exception->getMessage(), 36 | ] 37 | ); 38 | } 39 | })(); 40 | -------------------------------------------------------------------------------- /MergePullRequestPm/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mgonzalezbaile/merge-pull-request-pm", 3 | "description": "Manage the process of merging a PR", 4 | "license": "MIT", 5 | "require": { 6 | "php": "^7.2", 7 | "mgonzalezbaile/common": "dev-user-identity-bc", 8 | "soa-php/process-manager-middleware": "^1.0", 9 | "martinezdelariva/functional": "dev-master", 10 | "ext-json": "*", 11 | "moneyphp/money": "^3.1" 12 | }, 13 | "require-dev": { 14 | "friendsofphp/php-cs-fixer": "^2.12", 15 | "phpunit/phpunit": "^7.4" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "MergePullRequestPm\\": "src/" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "MergePullRequestPmTest\\": "tests/" 25 | } 26 | }, 27 | "repositories": [ 28 | { 29 | "type": "path", 30 | "url": "../Common", 31 | "options": { 32 | "symlink": false 33 | } 34 | } 35 | ], 36 | "config": { 37 | "bin-dir": "bin/" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MergePullRequestPm/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | COMMAND=${1:-} 4 | shift 5 | 6 | run_in_docker="docker run -it --rm -v $PWD/..:/srv/app -w /srv/app/MergePullRequestPm --user ${UID} mgonzalezbaile/php_base:1.0" 7 | 8 | case "$COMMAND" in 9 | composer) 10 | ${run_in_docker} composer $@ 11 | ;; 12 | phpunit) 13 | ${run_in_docker} vendor/phpunit/phpunit/phpunit tests 14 | ;; 15 | exec) 16 | ${run_in_docker} $@ 17 | ;; 18 | esac 19 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Application/MergePullRequestEventBus.php: -------------------------------------------------------------------------------- 1 | container = $container; 37 | $this->trace = $trace; 38 | } 39 | 40 | public function handle(DomainEvent $domainEvent): Transition 41 | { 42 | $processManagerName = $this->container->get('config')['service-name']; 43 | 44 | $pipeline = MiddlewarePipelineFactory::create( 45 | new PersistProcessMiddleware($this->container->get(Repository::class)), 46 | new PersistMessagesMiddleware( 47 | new ClockImpl(), 48 | $processManagerName, 49 | $this->container->get(OutgoingMessageStore::class), 50 | $this->container->get(IncomingMessageStore::class), 51 | $this->trace, 52 | $processManagerName, 53 | new UuidIdentifierGenerator() 54 | ), 55 | new DomainEventHandlerSelectorMiddleware($this->container, $this->container->get(Repository::class)) 56 | ); 57 | 58 | return $pipeline($domainEvent); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Domain/Command/CollectMoneyCommand.php: -------------------------------------------------------------------------------- 1 | payer = $payer; 39 | $this->amount = $amount; 40 | $this->subjectId = $subjectId; 41 | $this->currencyCode = $currencyCode; 42 | $this->aggregateRootId = $aggregateRootId; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Domain/Command/MergePullRequestCommand.php: -------------------------------------------------------------------------------- 1 | aggregateRootId = $aggregateRootId; 19 | } 20 | 21 | public function aggregateRootId(): string 22 | { 23 | return $this->aggregateRootId; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Domain/Command/PayMoneyCommand.php: -------------------------------------------------------------------------------- 1 | payee = $payee; 39 | $this->amount = $amount; 40 | $this->subjectId = $subjectId; 41 | $this->currencyCode = $currencyCode; 42 | $this->aggregateRootId = $aggregateRootId; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Domain/Command/RecipientAddress.php: -------------------------------------------------------------------------------- 1 | pullRequestId; 24 | } 25 | 26 | public function withPullRequestId(string $pullRequestId): self 27 | { 28 | $clone = clone $this; 29 | $clone->pullRequestId = $pullRequestId; 30 | 31 | return $clone; 32 | } 33 | 34 | public function pendingPayments(): array 35 | { 36 | return $this->pendingPayments; 37 | } 38 | 39 | public function withPendingPayments(array $pendingPayments): self 40 | { 41 | $clone = clone $this; 42 | $clone->pendingPayments = $pendingPayments; 43 | 44 | return $clone; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Domain/MergePullRequestState.php: -------------------------------------------------------------------------------- 1 | writer = $writer; 27 | $this->pullRequestId = $pullRequestId; 28 | $this->reviewers = $reviewers; 29 | } 30 | 31 | public function writer(): string 32 | { 33 | return $this->writer; 34 | } 35 | 36 | public function pullRequestId(): string 37 | { 38 | return $this->pullRequestId; 39 | } 40 | 41 | public function reviewers(): array 42 | { 43 | return $this->reviewers; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Domain/Provider/PullRequestProvider.php: -------------------------------------------------------------------------------- 1 | accountMoneyWasCollected($domainEvent, $process); 27 | $newState = MergePullRequestState::MERGING(); 28 | $nextCommands[] = CommandBuilder::buildCommand(new MergePullRequestCommand($process->pullRequestId())) 29 | ->withRecipient(RecipientAddress::PULL_REQUEST) 30 | ->withProcessId($process->id()) 31 | ->withStreamId($process->pullRequestId()) 32 | ->create(); 33 | 34 | if ($process->pendingPayments()) { 35 | $newState = $process->currentState(); 36 | $nextCommands = []; 37 | } 38 | 39 | if ($process->currentState()->isNot(MergePullRequestState::EXCHANGING_MONEY())) { 40 | InvalidStateTransitionException::ofProcess($process)->fromState($process->currentState())->toState($newState)->throw(); 41 | } 42 | 43 | return Transition::to($process->withState($newState))->withCommands($nextCommands); 44 | } 45 | 46 | private function accountMoneyWasCollected(DomainEvent $domainEvent, MergePullRequestProcess $process): MergePullRequestProcess 47 | { 48 | return $process->withPendingPayments(array_values(array_diff($process->pendingPayments(), [$domainEvent->streamId()]))); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Domain/UseCase/MoneyPayed.php: -------------------------------------------------------------------------------- 1 | accountMoneyWasPayed($domainEvent, $process); 27 | $newState = MergePullRequestState::MERGING(); 28 | $nextCommands[] = CommandBuilder::buildCommand(new MergePullRequestCommand($process->pullRequestId())) 29 | ->withRecipient(RecipientAddress::PULL_REQUEST) 30 | ->withProcessId($process->id()) 31 | ->withStreamId($process->pullRequestId()) 32 | ->create(); 33 | 34 | if ($process->pendingPayments()) { 35 | $newState = $process->currentState(); 36 | $nextCommands = []; 37 | } 38 | 39 | if ($process->currentState()->isNot(MergePullRequestState::EXCHANGING_MONEY())) { 40 | InvalidStateTransitionException::ofProcess($process)->fromState($process->currentState())->toState($newState)->throw(); 41 | } 42 | 43 | return Transition::to($process->withState($newState))->withCommands($nextCommands); 44 | } 45 | 46 | private function accountMoneyWasPayed(DomainEvent $domainEvent, MergePullRequestProcess $process): MergePullRequestProcess 47 | { 48 | return $process->withPendingPayments(array_values(array_diff($process->pendingPayments(), [$domainEvent->streamId()]))); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Domain/UseCase/PullRequestMarkedAsMergeable.php: -------------------------------------------------------------------------------- 1 | withState($newState))->withCommands([]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Di/ZendServiceManager/Alias/MergePullRequestPmProjectionTable.php: -------------------------------------------------------------------------------- 1 | get(PullRequestProvider::class), new PricingProviderInMemory()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Di/ZendServiceManager/Factory/PullRequestProviderMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get(Client::class); 17 | $database = $client->selectDatabase('code_review')->withOptions(['typeMap' =>['document' => 'array', 'root' => 'array']]); 18 | 19 | return new PullRequestProviderMongoDb($database->selectCollection('pull_requests')); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Di/ZendServiceManager/Factory/RepositoryMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 20 | $processManagerCollection = $database->selectCollection('process_managers'); 21 | 22 | return new RepositoryMongoDb($processManagerCollection, MergePullRequestProcess::class, MergePullRequestState::class, new UuidIdentifierGenerator()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Di/ZendServiceManager/autoload/config.local.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'factories' => [ 12 | ErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class, 13 | 'Zend\Expressive\Whoops' => Container\WhoopsFactory::class, 14 | 'Zend\Expressive\WhoopsPageHandler' => Container\WhoopsPageHandlerFactory::class, 15 | ], 16 | ], 17 | 'whoops' => [ 18 | 'json_exceptions' => [ 19 | 'display' => true, 20 | 'show_trace' => true, 21 | 'ajax_only' => true, 22 | ], 23 | ], 24 | 'debug' => true, 25 | ConfigAggregator::ENABLE_CACHE => false, 26 | ]; 27 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Di/ZendServiceManager/config.php: -------------------------------------------------------------------------------- 1 | 'data/cache/config-cache.php', 13 | ]; 14 | 15 | $aggregator = new ConfigAggregator([ 16 | \Zend\Expressive\Router\FastRouteRouter\ConfigProvider::class, 17 | \Zend\HttpHandlerRunner\ConfigProvider::class, 18 | Zend\ProblemDetails\ConfigProvider::class, 19 | new ArrayProvider($cacheConfig), 20 | \Zend\Expressive\Helper\ConfigProvider::class, 21 | \Zend\Expressive\ConfigProvider::class, 22 | \Zend\Expressive\Router\ConfigProvider::class, 23 | new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'), 24 | new PhpFileProvider(realpath(__DIR__) . '/development.config.php'), 25 | ], $cacheConfig['config_cache_path']); 26 | 27 | return $aggregator->getMergedConfig(); 28 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Di/ZendServiceManager/container.php: -------------------------------------------------------------------------------- 1 | configureServiceManager($container); 11 | $container->setService('config', $config); 12 | 13 | return $container; 14 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Persistence/InMemory/PricingProviderInMemory.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 21 | } 22 | 23 | public function ofId(string $pullRequestId): PullRequest 24 | { 25 | $result = $this->collection->findOne(['id' => $pullRequestId]); 26 | 27 | if (empty($result)) { 28 | throw new \RuntimeException("Pull Request $pullRequestId not found"); 29 | } 30 | 31 | return new PullRequest($result['writer'], $result['id'], $result['approvers']); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Persistence/MongoDb/RepositoryMongoDb.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 40 | $this->processFqcn = $processFqcn; 41 | $this->identifierGenerator = $identifierGenerator; 42 | $this->stateFqcn = $stateFqcn; 43 | } 44 | 45 | public function findOfId(string $id): Process 46 | { 47 | $result = $this->collection->findOne(['id' => $id]); 48 | 49 | if (empty($result)) { 50 | return hydrate($this->processFqcn, ['id' => $this->identifierGenerator->nextIdentity(), 'state' => State::INITIALIZED()]); 51 | } 52 | 53 | $state = $result['state']; 54 | $result['state'] = $this->stateFqcn::$state(); 55 | 56 | return hydrate($this->processFqcn, $result); 57 | } 58 | 59 | public function save(Process $process): void 60 | { 61 | $data = extract($process); 62 | $data['state'] = $process->currentState()->getName(); 63 | 64 | $this->collection->replaceOne(['_id' => $process->id()], $data, ['upsert' => true]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Ui/Messaging/Listener/AbstractMessageListener.php: -------------------------------------------------------------------------------- 1 | container = $container; 28 | } 29 | 30 | public function handle(Message $message): void 31 | { 32 | $this->message = $message; 33 | 34 | $this->handleMessage($message); 35 | } 36 | 37 | protected function eventBus(string $eventBusFqcn): EventBus 38 | { 39 | $trace = new Trace( 40 | $this->message->id(), 41 | $this->message->occurredOn(), 42 | $this->message->correlationId(), 43 | $this->message->causationId(), 44 | $this->message->replyTo(), 45 | $this->message->processId() 46 | ); 47 | 48 | return new $eventBusFqcn($this->container, $trace); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Ui/Messaging/Listener/MoneyCollectedListener.php: -------------------------------------------------------------------------------- 1 | event($message)->withProcessId($message->processId())->withStreamId($message->streamId()); 17 | 18 | $this->eventBus(MergePullRequestEventBus::class)->handle($event); 19 | } 20 | 21 | private function event(Message $message): MoneyCollected 22 | { 23 | return $event = hydrate(MoneyCollected::class, $message->body()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Ui/Messaging/Listener/MoneyPayedListener.php: -------------------------------------------------------------------------------- 1 | event($message)->withProcessId($message->processId())->withStreamId($message->streamId()); 17 | 18 | $this->eventBus(MergePullRequestEventBus::class)->handle($event); 19 | } 20 | 21 | private function event(Message $message): MoneyPayed 22 | { 23 | return $event = hydrate(MoneyPayed::class, $message->body()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Ui/Messaging/Listener/PullRequestMarkedAsMergeableListener.php: -------------------------------------------------------------------------------- 1 | event($message)->withProcessId($message->processId())->withStreamId($message->streamId()); 17 | 18 | $this->eventBus(MergePullRequestEventBus::class)->handle($event); 19 | } 20 | 21 | private function event(Message $message): PullRequestMarkedAsMergeable 22 | { 23 | return $event = hydrate(PullRequestMarkedAsMergeable::class, $message->body()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Ui/Messaging/Listener/PullRequestMergedListener.php: -------------------------------------------------------------------------------- 1 | event($message)->withProcessId($message->processId())->withStreamId($message->streamId()); 17 | 18 | $this->eventBus(MergePullRequestEventBus::class)->handle($event); 19 | } 20 | 21 | private function event(Message $message): PullRequestMerged 22 | { 23 | return $event = hydrate(PullRequestMerged::class, $message->body()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MergePullRequestPm/src/Infrastructure/Ui/Messaging/routes.php: -------------------------------------------------------------------------------- 1 | add(, , ); 14 | $application->addSubscription( 15 | 'pull_request', 16 | 'com.pull_request.events.pull_request_marked_as_mergeable', 17 | PullRequestMarkedAsMergeableListener::class 18 | ); 19 | $application->addSubscription( 20 | 'payment', 21 | 'com.payment.events.money_collected', 22 | MoneyCollectedListener::class 23 | ); 24 | $application->addSubscription( 25 | 'payment', 26 | 'com.payment.events.money_payed', 27 | MoneyPayedListener::class 28 | ); 29 | $application->addSubscription( 30 | 'pull_request', 31 | 'com.pull_request.events.pull_request_merged', 32 | PullRequestMergedListener::class 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /MergePullRequestPm/tests/UseCase/PullRequestMergedHandlerTest.php: -------------------------------------------------------------------------------- 1 | scenario->withDomainEventHandler(new PullRequestMergedHandler()); 19 | } 20 | 21 | /** 22 | * @test 23 | */ 24 | public function shouldTransitToFinished(): void 25 | { 26 | $process = new MergePullRequestProcess(); 27 | $this->scenario 28 | ->given($process) 29 | ->when((new PullRequestMerged())->withStreamId('some stream id')) 30 | ->then(Transition::to($process->withState(MergePullRequestState::FINISHED()))->withCommands([])); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MergePullRequestPm/var/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soa-php/code-review-app/4c11a84f745eb9257d30b6b56860e622c4918183/MergePullRequestPm/var/.gitkeep -------------------------------------------------------------------------------- /MessagePublisher/bin/publish-messages: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 | get(LoggerInterface::class); 25 | $app = new PublisherApplication($container->get(MessagePublisher::class),$container->get(MessageDeliveryService::class),$logger, new PcntlLoopFactory()); 26 | 27 | try { 28 | $app->startPublishing(); 29 | } catch (\Throwable $exception) { 30 | /** @var LoggerInterface $logger */ 31 | $logger->error( 32 | $exception->getMessage(), 33 | [ 34 | 'exception' => $exception, 35 | 'message' => $exception->getMessage(), 36 | ] 37 | ); 38 | } 39 | })(); 40 | -------------------------------------------------------------------------------- /MessagePublisher/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mgonzalezbaile/message-publisher", 3 | "description": "Component in charge of publishing persisted messages", 4 | "license": "MIT", 5 | "require": { 6 | "php": "^7.2", 7 | "mgonzalezbaile/common": "dev-user-identity-bc", 8 | "martinezdelariva/functional": "dev-master", 9 | "ext-json": "*" 10 | }, 11 | "require-dev": { 12 | "friendsofphp/php-cs-fixer": "^2.12", 13 | "phpunit/phpunit": "^7.4" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "MessagePublisher\\": "src/" 18 | } 19 | }, 20 | "repositories": [ 21 | { 22 | "type": "path", 23 | "url": "../Common", 24 | "options": { 25 | "symlink": false 26 | } 27 | } 28 | ], 29 | "config": { 30 | "bin-dir": "bin/" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MessagePublisher/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | COMMAND=${1:-} 4 | shift 5 | 6 | run_in_docker="docker run -it --rm -v $PWD/..:/srv/app -w /srv/app/MessagePublisher --user ${UID} mgonzalezbaile/php_base:1.0" 7 | 8 | case "$COMMAND" in 9 | composer) 10 | ${run_in_docker} composer $@ 11 | ;; 12 | phpunit) 13 | ${run_in_docker} vendor/phpunit/phpunit/phpunit tests 14 | ;; 15 | esac 16 | -------------------------------------------------------------------------------- /MessagePublisher/src/Infrastructure/Di/ZendServiceManager/Factory/AmqpMessagePublisherFactory.php: -------------------------------------------------------------------------------- 1 | get('config')['message-recipients']; 16 | 17 | return new AmqpMessagePublisher(new AmqpPublisherConfig('message-publisher', $container->get('config')['rabbitmq']['credentials'], $recipients)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MessagePublisher/src/Infrastructure/Di/ZendServiceManager/autoload/config.local.php: -------------------------------------------------------------------------------- 1 | true, 11 | ConfigAggregator::ENABLE_CACHE => false, 12 | ]; 13 | -------------------------------------------------------------------------------- /MessagePublisher/src/Infrastructure/Di/ZendServiceManager/config.php: -------------------------------------------------------------------------------- 1 | 'data/cache/config-cache.php', 13 | ]; 14 | 15 | $aggregator = new ConfigAggregator([ 16 | \Zend\Expressive\Router\FastRouteRouter\ConfigProvider::class, 17 | \Zend\HttpHandlerRunner\ConfigProvider::class, 18 | Zend\ProblemDetails\ConfigProvider::class, 19 | new ArrayProvider($cacheConfig), 20 | \Zend\Expressive\Helper\ConfigProvider::class, 21 | \Zend\Expressive\ConfigProvider::class, 22 | \Zend\Expressive\Router\ConfigProvider::class, 23 | new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'), 24 | new PhpFileProvider(realpath(__DIR__) . '/development.config.php'), 25 | ], $cacheConfig['config_cache_path']); 26 | 27 | return $aggregator->getMergedConfig(); 28 | -------------------------------------------------------------------------------- /MessagePublisher/src/Infrastructure/Di/ZendServiceManager/container.php: -------------------------------------------------------------------------------- 1 | configureServiceManager($container); 11 | $container->setService('config', $config); 12 | 13 | return $container; 14 | -------------------------------------------------------------------------------- /Payment/bin/listen-messages: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 | get(MessageRouter::class), $container->get(MessageSubscriber::class), new PcntlLoopFactory()); 25 | 26 | (require __DIR__ . '/../src/Infrastructure/Ui/Messaging/routes.php')($app, $container); 27 | 28 | try { 29 | $app->startConsuming(); 30 | } catch (\Throwable $exception) { 31 | /** @var LoggerInterface $logger */ 32 | $logger = $container->get(LoggerInterface::class); 33 | $logger->error( 34 | $exception->getMessage(), 35 | [ 36 | 'exception' => $exception, 37 | 'message' => $exception->getMessage(), 38 | ] 39 | ); 40 | } 41 | })(); 42 | -------------------------------------------------------------------------------- /Payment/bin/publish-messages: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 | get(LoggerInterface::class); 25 | $app = new PublisherApplication($container->get(MessagePublisher::class),$container->get(MessageDeliveryService::class),$logger, new PcntlLoopFactory()); 26 | 27 | try { 28 | $app->startPublishing(); 29 | } catch (\Throwable $exception) { 30 | /** @var LoggerInterface $logger */ 31 | $logger->error( 32 | $exception->getMessage(), 33 | [ 34 | 'exception' => $exception, 35 | 'message' => $exception->getMessage(), 36 | ] 37 | ); 38 | } 39 | })(); 40 | -------------------------------------------------------------------------------- /Payment/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mgonzalezbaile/payment", 3 | "description": "Payment Bounded Context", 4 | "license": "MIT", 5 | "require": { 6 | "php": "^7.2", 7 | "mgonzalezbaile/common": "dev-user-identity-bc", 8 | "martinezdelariva/functional": "dev-master", 9 | "ext-json": "*" 10 | }, 11 | "require-dev": { 12 | "friendsofphp/php-cs-fixer": "^2.12", 13 | "phpunit/phpunit": "^7.4" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Payment\\": "src/" 18 | } 19 | }, 20 | "repositories": [ 21 | { 22 | "type": "path", 23 | "url": "../Common", 24 | "options": { 25 | "symlink": false 26 | } 27 | } 28 | ], 29 | "config": { 30 | "bin-dir": "bin/" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Payment/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | COMMAND=${1:-} 4 | shift 5 | 6 | run_in_docker="docker run -it --rm -v $PWD/..:/srv/app -w /srv/app/Payment --user ${UID} mgonzalezbaile/php_base:1.0" 7 | 8 | case "$COMMAND" in 9 | composer) 10 | ${run_in_docker} composer $@ 11 | ;; 12 | phpunit) 13 | ${run_in_docker} vendor/phpunit/phpunit/phpunit tests 14 | ;; 15 | esac 16 | -------------------------------------------------------------------------------- /Payment/src/Domain/Event/MoneyCollected.php: -------------------------------------------------------------------------------- 1 | id = $id; 49 | $this->payer = $payer; 50 | $this->amount = $amount; 51 | $this->currencyCode = $currencyCode; 52 | $this->subjectId = $subjectId; 53 | $this->provider = $provider; 54 | $this->providerTransactionId = $providerTransactionId; 55 | } 56 | 57 | public function streamId(): string 58 | { 59 | return $this->id; 60 | } 61 | 62 | public function payer(): string 63 | { 64 | return $this->payer; 65 | } 66 | 67 | public function amount(): string 68 | { 69 | return $this->amount; 70 | } 71 | 72 | public function currencyCode(): string 73 | { 74 | return $this->currencyCode; 75 | } 76 | 77 | public function subjectId(): string 78 | { 79 | return $this->subjectId; 80 | } 81 | 82 | public function provider(): string 83 | { 84 | return $this->provider; 85 | } 86 | 87 | public function providerTransactionId(): string 88 | { 89 | return $this->providerTransactionId; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Payment/src/Domain/Event/MoneyPayed.php: -------------------------------------------------------------------------------- 1 | id = $id; 49 | $this->payee = $payee; 50 | $this->amount = $amount; 51 | $this->currencyCode = $currencyCode; 52 | $this->subjectId = $subjectId; 53 | $this->provider = $provider; 54 | $this->providerTransactionId = $providerTransactionId; 55 | } 56 | 57 | public function streamId(): string 58 | { 59 | return $this->id; 60 | } 61 | 62 | public function payee(): string 63 | { 64 | return $this->payee; 65 | } 66 | 67 | public function amount(): string 68 | { 69 | return $this->amount; 70 | } 71 | 72 | public function currencyCode(): string 73 | { 74 | return $this->currencyCode; 75 | } 76 | 77 | public function subjectId(): string 78 | { 79 | return $this->subjectId; 80 | } 81 | 82 | public function provider(): string 83 | { 84 | return $this->provider; 85 | } 86 | 87 | public function providerTransactionId(): string 88 | { 89 | return $this->providerTransactionId; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Payment/src/Domain/Event/PayMoneyFailed.php: -------------------------------------------------------------------------------- 1 | id = $id; 55 | $this->payee = $payee; 56 | $this->amount = $amount; 57 | $this->currencyCode = $currencyCode; 58 | $this->subjectId = $subjectId; 59 | $this->provider = $provider; 60 | $this->providerTransactionId = $providerTransactionId; 61 | $this->reason = $reason; 62 | } 63 | 64 | public function streamId(): string 65 | { 66 | return $this->id; 67 | } 68 | 69 | public function payee(): string 70 | { 71 | return $this->payee; 72 | } 73 | 74 | public function amount(): string 75 | { 76 | return $this->amount; 77 | } 78 | 79 | public function currencyCode(): string 80 | { 81 | return $this->currencyCode; 82 | } 83 | 84 | public function subjectId(): string 85 | { 86 | return $this->subjectId; 87 | } 88 | 89 | public function provider(): string 90 | { 91 | return $this->provider; 92 | } 93 | 94 | public function providerTransactionId(): string 95 | { 96 | return $this->providerTransactionId; 97 | } 98 | 99 | public function reason(): string 100 | { 101 | return $this->reason; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Payment/src/Domain/PaymentProvider.php: -------------------------------------------------------------------------------- 1 | id = $id; 37 | $this->wasSucceed = $wasSucceed; 38 | $this->failureReason = $failureReason; 39 | } 40 | 41 | public function id(): string 42 | { 43 | return $this->id; 44 | } 45 | 46 | public function wasSucceed(): bool 47 | { 48 | return $this->wasSucceed; 49 | } 50 | 51 | public function failureReason(): string 52 | { 53 | return $this->failureReason; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Payment/src/Domain/UseCase/CollectMoneyCommand.php: -------------------------------------------------------------------------------- 1 | payer = $payer; 34 | $this->amount = $amount; 35 | $this->subjectId = $subjectId; 36 | $this->currencyCode = $currencyCode; 37 | } 38 | 39 | public function payer(): string 40 | { 41 | return $this->payer; 42 | } 43 | 44 | public function amount(): string 45 | { 46 | return $this->amount; 47 | } 48 | 49 | public function subjectId(): string 50 | { 51 | return $this->subjectId; 52 | } 53 | 54 | public function currencyCode(): string 55 | { 56 | return $this->currencyCode; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Payment/src/Domain/UseCase/CollectMoneyCommandHandler.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 25 | } 26 | 27 | /** 28 | * @param CollectMoneyCommand $command 29 | */ 30 | public function handle(Command $command, AggregateRoot $state = null): EventStream 31 | { 32 | $transaction = $this->provider->collect($command->payer(), $command->amount(), $command->currencyCode(), $command->subjectId()); 33 | if ($transaction->wasSucceed()) { 34 | return EventStream::fromDomainEvents(new MoneyCollected( 35 | $command->aggregateRootId(), 36 | $command->payer(), 37 | $command->amount(), 38 | $command->currencyCode(), 39 | $command->subjectId(), 40 | $this->provider->identifier(), 41 | $transaction->id()) 42 | ); 43 | } 44 | 45 | return EventStream::fromDomainEvents(new CollectMoneyFailed( 46 | $command->aggregateRootId(), 47 | $command->payer(), 48 | $command->amount(), 49 | $command->currencyCode(), 50 | $command->subjectId(), 51 | $this->provider->identifier(), 52 | $transaction->id(), 53 | $transaction->failureReason() 54 | )); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Payment/src/Domain/UseCase/PayMoneyCommand.php: -------------------------------------------------------------------------------- 1 | payee = $payee; 34 | $this->amount = $amount; 35 | $this->subjectId = $subjectId; 36 | $this->currencyCode = $currencyCode; 37 | } 38 | 39 | public function payee(): string 40 | { 41 | return $this->payee; 42 | } 43 | 44 | public function amount(): string 45 | { 46 | return $this->amount; 47 | } 48 | 49 | public function subjectId(): string 50 | { 51 | return $this->subjectId; 52 | } 53 | 54 | public function currencyCode(): string 55 | { 56 | return $this->currencyCode; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Payment/src/Domain/UseCase/PayMoneyCommandHandler.php: -------------------------------------------------------------------------------- 1 | provider = $provider; 25 | } 26 | 27 | /** 28 | * @param PayMoneyCommand $command 29 | */ 30 | public function handle(Command $command, AggregateRoot $state = null): EventStream 31 | { 32 | $transaction = $this->provider->payout($command->payee(), $command->amount(), $command->currencyCode(), $command->subjectId()); 33 | if ($transaction->wasSucceed()) { 34 | return EventStream::fromDomainEvents(new MoneyPayed( 35 | $command->aggregateRootId(), 36 | $command->payee(), 37 | $command->amount(), 38 | $command->currencyCode(), 39 | $command->subjectId(), 40 | $this->provider->identifier(), 41 | $transaction->id()) 42 | ); 43 | } 44 | 45 | return EventStream::fromDomainEvents(new PayMoneyFailed( 46 | $command->aggregateRootId(), 47 | $command->payee(), 48 | $command->amount(), 49 | $command->currencyCode(), 50 | $command->subjectId(), 51 | $this->provider->identifier(), 52 | $transaction->id(), 53 | $transaction->failureReason() 54 | )); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Di/ZendServiceManager/Alias/PaymentProjectionTable.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 18 | $collection = $database->selectCollection('payments'); 19 | 20 | return new ProjectionTableMongoDb($collection); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Di/ZendServiceManager/Factory/PaymentRepositoryMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 18 | $collection = $database->selectCollection('pull_requests'); 19 | 20 | return new RepositoryMongoDb($collection, ''); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Di/ZendServiceManager/autoload/config.local.php: -------------------------------------------------------------------------------- 1 | true, 11 | ConfigAggregator::ENABLE_CACHE => false, 12 | ]; 13 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Di/ZendServiceManager/config.php: -------------------------------------------------------------------------------- 1 | 'data/cache/config-cache.php', 13 | ]; 14 | 15 | $aggregator = new ConfigAggregator([ 16 | \Zend\Expressive\Router\FastRouteRouter\ConfigProvider::class, 17 | \Zend\HttpHandlerRunner\ConfigProvider::class, 18 | Zend\ProblemDetails\ConfigProvider::class, 19 | new ArrayProvider($cacheConfig), 20 | \Zend\Expressive\Helper\ConfigProvider::class, 21 | \Zend\Expressive\ConfigProvider::class, 22 | \Zend\Expressive\Router\ConfigProvider::class, 23 | new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'), 24 | new PhpFileProvider(realpath(__DIR__) . '/development.config.php'), 25 | ], $cacheConfig['config_cache_path']); 26 | 27 | return $aggregator->getMergedConfig(); 28 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Di/ZendServiceManager/container.php: -------------------------------------------------------------------------------- 1 | configureServiceManager($container); 11 | $container->setService('config', $config); 12 | 13 | return $container; 14 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Domain/PaymentProviderFake.php: -------------------------------------------------------------------------------- 1 | transaction = empty($transaction) ? Transaction::succeed('transaction id') : $transaction; 27 | } 28 | 29 | public function collect(string $payer, string $amount, string $currencyCode, string $subjectId): Transaction 30 | { 31 | return $this->transaction; 32 | } 33 | 34 | public function payout(string $payee, string $amount, string $currencyCode, string $subjectId): Transaction 35 | { 36 | return $this->transaction; 37 | } 38 | 39 | public function identifier(): string 40 | { 41 | return self::ID; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Persistence/MongoDb/ProjectionTableMongoDb.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 21 | } 22 | 23 | public function findOfId(string $id): array 24 | { 25 | $result = $this->collection->findOne(['id' => $id]); 26 | 27 | return empty($result) ? [] : $result; 28 | } 29 | 30 | public function save(string $id, array $projection): void 31 | { 32 | $projection = $this->setTime($projection); 33 | 34 | $this->collection->replaceOne(['id' => $id], $projection, ['upsert' => true]); 35 | } 36 | 37 | private function setTime(array $projection): array 38 | { 39 | $now = new UTCDateTime(); 40 | 41 | if (!isset($projection['createdAt'])) { 42 | $projection['createdAt'] = $now; 43 | } 44 | 45 | $projection['updatedAt'] = $now; 46 | 47 | return $projection; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Persistence/MongoDb/RepositoryMongoDb.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 27 | $this->stateFqcn = $stateFqcn; 28 | } 29 | 30 | public function findOfId(string $id): ?AggregateRoot 31 | { 32 | $result = $this->collection->findOne(['id' => $id]); 33 | 34 | return empty($result) ? null : hydrate($this->stateFqcn, $result); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Ui/Http/Restful/Resource/CollectMoneyResource.php: -------------------------------------------------------------------------------- 1 | buildCommand($request->getBody()->getContents())->withAggregateRootId($this->identifierGenerator->nextIdentity()); 25 | 26 | $result = $this->commandBus(PaymentCommandBus::class)->handle($command); 27 | 28 | return $this->buildResponse($request, $result); 29 | } 30 | 31 | private function buildCommand(string $body): CollectMoneyCommand 32 | { 33 | return hydrate(CollectMoneyCommand::class, json_decode($body, true)); 34 | } 35 | 36 | private function buildResponse(ServerRequestInterface $request, CommandResponse $result): ResponseInterface 37 | { 38 | $pattern = [ 39 | CollectMoneyFailed::class => function (CollectMoneyFailed $event) use ($request) { 40 | return $this->problemDetailsResponseFactory->createResponse( 41 | $request, 42 | Httpstatuscodes::HTTP_CONFLICT, 43 | $event->reason() 44 | ); 45 | }, 46 | 47 | _ => function (DomainEvent $domainEvent) use ($request) { 48 | return new EmptyResponse(Httpstatuscodes::HTTP_NO_CONTENT); 49 | }, 50 | ]; 51 | 52 | return match($pattern, $result->eventStream()->first()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Ui/Http/Restful/Resource/PayoutMoneyResource.php: -------------------------------------------------------------------------------- 1 | buildCommand($request->getBody()->getContents())->withAggregateRootId($this->identifierGenerator->nextIdentity()); 25 | 26 | $result = $this->commandBus(PaymentCommandBus::class)->handle($command); 27 | 28 | return $this->buildResponse($request, $result); 29 | } 30 | 31 | private function buildCommand(string $body): PayMoneyCommand 32 | { 33 | return hydrate(PayMoneyCommand::class, json_decode($body, true)); 34 | } 35 | 36 | private function buildResponse(ServerRequestInterface $request, CommandResponse $result): ResponseInterface 37 | { 38 | $pattern = [ 39 | PayMoneyFailed::class => function (PayMoneyFailed $event) use ($request) { 40 | return $this->problemDetailsResponseFactory->createResponse( 41 | $request, 42 | Httpstatuscodes::HTTP_CONFLICT, 43 | $event->reason() 44 | ); 45 | }, 46 | 47 | _ => function (DomainEvent $domainEvent) use ($request) { 48 | return new EmptyResponse(Httpstatuscodes::HTTP_NO_CONTENT); 49 | }, 50 | ]; 51 | 52 | return match($pattern, $result->eventStream()->first()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Ui/Http/Restful/routes.php: -------------------------------------------------------------------------------- 1 | get('/', App\Handler\HomePageHandler::class, 'home'); 15 | * $app->post('/album', App\Handler\AlbumCreateHandler::class, 'album.create'); 16 | * $app->put('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.put'); 17 | * $app->patch('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.patch'); 18 | * $app->delete('/album/:id', App\Handler\AlbumDeleteHandler::class, 'album.delete'); 19 | * 20 | * Or with multiple request methods: 21 | * 22 | * $app->route('/contact', App\Handler\ContactHandler::class, ['GET', 'POST', ...], 'contact'); 23 | * 24 | * Or handling all request methods: 25 | * 26 | * $app->route('/contact', App\Handler\ContactHandler::class)->setName('contact'); 27 | * 28 | * or: 29 | * 30 | * $app->route( 31 | * '/contact', 32 | * App\Handler\ContactHandler::class, 33 | * Zend\Expressive\Router\Route::HTTP_METHOD_ANY, 34 | * 'contact' 35 | * ); 36 | */ 37 | return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void { 38 | $app->post('/payments/collect', CollectMoneyResource::class, CollectMoneyResource::class); 39 | $app->post('/payments/payout', PayoutMoneyResource::class, PayoutMoneyResource::class); 40 | }; 41 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Ui/Http/index.php: -------------------------------------------------------------------------------- 1 | get(\Zend\Expressive\Application::class); 22 | $factory = $container->get(\Zend\Expressive\MiddlewareFactory::class); 23 | 24 | // Execute programmatic/declarative middleware pipeline and routing 25 | // configuration statements 26 | (require __DIR__ . '/pipeline.php')($app, $factory, $container); 27 | (require __DIR__ . '/Restful/routes.php')($app, $factory, $container); 28 | 29 | $app->run(); 30 | })(); 31 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Ui/Messaging/Listener/CollectMoneyCommandListener.php: -------------------------------------------------------------------------------- 1 | body()); 21 | 22 | /** @var CommandResponse $result */ 23 | $result = $this->commandBus(PaymentCommandBus::class, $message)->handle($command); 24 | 25 | $pattern = [ 26 | FailureDomainEvent::class => function (FailureDomainEvent $event) { 27 | throw new \Exception($event->reason()); 28 | }, 29 | ]; 30 | 31 | match($pattern, $result->eventStream()->first()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Ui/Messaging/Listener/PayMoneyCommandListener.php: -------------------------------------------------------------------------------- 1 | body()); 21 | 22 | /** @var CommandResponse $result */ 23 | $result = $this->commandBus(PaymentCommandBus::class, $message)->handle($command); 24 | 25 | $pattern = [ 26 | FailureDomainEvent::class => function (FailureDomainEvent $event) { 27 | throw new \Exception($event->reason()); 28 | }, 29 | ]; 30 | 31 | match($pattern, $result->eventStream()->first()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Payment/src/Infrastructure/Ui/Messaging/routes.php: -------------------------------------------------------------------------------- 1 | add(, , , ); 12 | $boundedContext = $container->get('config')['service-name']; 13 | 14 | $application->addSubscription($boundedContext, 'com.payment.commands.collect_money_command', CollectMoneyCommandListener::class); 15 | $application->addSubscription($boundedContext, 'com.payment.commands.pay_money_command', PayMoneyCommandListener::class); 16 | }; 17 | -------------------------------------------------------------------------------- /PullRequest/bin/listen-messages: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 | get(MessageRouter::class), $container->get(MessageSubscriber::class), new PcntlLoopFactory()); 25 | 26 | (require __DIR__ . '/../src/Infrastructure/Ui/Messaging/routes.php')($app, $container); 27 | 28 | try { 29 | $app->startConsuming(); 30 | } catch (\Throwable $exception) { 31 | /** @var LoggerInterface $logger */ 32 | $logger = $container->get(LoggerInterface::class); 33 | $logger->error( 34 | $exception->getMessage(), 35 | [ 36 | 'exception' => $exception, 37 | 'message' => $exception->getMessage(), 38 | ] 39 | ); 40 | } 41 | })(); 42 | -------------------------------------------------------------------------------- /PullRequest/bin/publish-messages: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 3 | get(LoggerInterface::class); 25 | $app = new PublisherApplication($container->get(MessagePublisher::class),$container->get(MessageDeliveryService::class),$logger, new PcntlLoopFactory()); 26 | 27 | try { 28 | $app->startPublishing(); 29 | } catch (\Throwable $exception) { 30 | /** @var LoggerInterface $logger */ 31 | $logger->error( 32 | $exception->getMessage(), 33 | [ 34 | 'exception' => $exception, 35 | 'message' => $exception->getMessage(), 36 | ] 37 | ); 38 | } 39 | })(); 40 | -------------------------------------------------------------------------------- /PullRequest/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mgonzalezbaile/pull-request", 3 | "description": "Pull Request Bounded Context", 4 | "license": "MIT", 5 | "require": { 6 | "php": "^7.2", 7 | "mgonzalezbaile/common": "dev-master", 8 | "martinezdelariva/functional": "dev-master", 9 | "ext-json": "*" 10 | }, 11 | "require-dev": { 12 | "friendsofphp/php-cs-fixer": "^2.12", 13 | "phpunit/phpunit": "^7.4" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "PullRequest\\": "src/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "PullRequestTest\\": "tests/" 23 | } 24 | }, 25 | "repositories": [ 26 | { 27 | "type": "path", 28 | "url": "../Common", 29 | "options": { 30 | "symlink": false 31 | } 32 | } 33 | ], 34 | "config": { 35 | "bin-dir": "bin/" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /PullRequest/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | COMMAND=${1:-} 4 | shift 5 | 6 | run_in_docker="docker run -it --rm -v $PWD/..:/srv/app -w /srv/app/PullRequest --user ${UID} mgonzalezbaile/php_base:1.0" 7 | 8 | case "$COMMAND" in 9 | composer) 10 | ${run_in_docker} composer $@ 11 | ;; 12 | phpunit) 13 | ${run_in_docker} vendor/phpunit/phpunit/phpunit tests 14 | ;; 15 | esac 16 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/Event/ApprovePullRequestFailed.php: -------------------------------------------------------------------------------- 1 | id = $id; 34 | $this->reviewer = $reviewer; 35 | $this->reason = $reason; 36 | } 37 | 38 | public function streamId(): string 39 | { 40 | return $this->id; 41 | } 42 | 43 | public function reason(): string 44 | { 45 | return $this->reason; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/Event/MergePullRequestFailed.php: -------------------------------------------------------------------------------- 1 | id = $id; 28 | $this->reason = $reason; 29 | } 30 | 31 | public function streamId(): string 32 | { 33 | return $this->id; 34 | } 35 | 36 | public function reason(): string 37 | { 38 | return $this->reason; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/Event/PullRequestApproved.php: -------------------------------------------------------------------------------- 1 | id = $id; 24 | $this->approver = $approver; 25 | } 26 | 27 | public function approver(): string 28 | { 29 | return $this->approver; 30 | } 31 | 32 | public function streamId(): string 33 | { 34 | return $this->id; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/Event/PullRequestCreated.php: -------------------------------------------------------------------------------- 1 | id = $id; 29 | $this->writer = $writer; 30 | $this->code = $code; 31 | } 32 | 33 | public function streamId(): string 34 | { 35 | return $this->id; 36 | } 37 | 38 | public function writer(): string 39 | { 40 | return $this->writer; 41 | } 42 | 43 | public function code(): string 44 | { 45 | return $this->code; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/Event/PullRequestCreationFailed.php: -------------------------------------------------------------------------------- 1 | writer = $writer; 33 | $this->code = $code; 34 | $this->reason = $reason; 35 | } 36 | 37 | public function streamId(): string 38 | { 39 | return 'n/a'; 40 | } 41 | 42 | public function reason(): string 43 | { 44 | return $this->reason; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/Event/PullRequestMarkedAsMergeable.php: -------------------------------------------------------------------------------- 1 | id = $id; 19 | } 20 | 21 | public function streamId(): string 22 | { 23 | return $this->id; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/Event/PullRequestMerged.php: -------------------------------------------------------------------------------- 1 | id = $id; 19 | } 20 | 21 | public function streamId(): string 22 | { 23 | return $this->id; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/Event/PullRequestReviewerAssignationFailed.php: -------------------------------------------------------------------------------- 1 | id = $id; 34 | $this->reviewer = $reviewer; 35 | $this->reason = $reason; 36 | } 37 | 38 | public function streamId(): string 39 | { 40 | return $this->id; 41 | } 42 | 43 | public function reason(): string 44 | { 45 | return $this->reason; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/Event/PullRequestReviewerAssigned.php: -------------------------------------------------------------------------------- 1 | id = $id; 24 | $this->reviewer = $reviewer; 25 | } 26 | 27 | public function streamId(): string 28 | { 29 | return $this->id; 30 | } 31 | 32 | public function reviewer(): string 33 | { 34 | return $this->reviewer; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/UseCase/ApprovePullRequestCommand.php: -------------------------------------------------------------------------------- 1 | approver = $reviewer; 24 | $this->pullRequestId = $pullRequestId; 25 | $this->aggregateRootId = $pullRequestId; 26 | } 27 | 28 | public function approver(): string 29 | { 30 | return $this->approver; 31 | } 32 | 33 | public function pullRequestId(): string 34 | { 35 | return $this->pullRequestId; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/UseCase/ApprovePullRequestCommandHandler.php: -------------------------------------------------------------------------------- 1 | mergeable()) { 25 | return EventStream::fromDomainEvents(new ApprovePullRequestFailed($command->pullRequestId(), $command->approver(), ApprovePullRequestFailed::ALREADY_MARKED_AS_MERGEABLE)); 26 | } 27 | 28 | if (!in_array($command->approver(), $pullRequest->assignedReviewers())) { 29 | return EventStream::fromDomainEvents(new ApprovePullRequestFailed($command->pullRequestId(), $command->approver(), ApprovePullRequestFailed::APPROVER_IS_NOT_REVIEWER)); 30 | } 31 | 32 | if (in_array($command->approver(), $pullRequest->approvers())) { 33 | return EventStream::fromDomainEvents(new ApprovePullRequestFailed($command->pullRequestId(), $command->approver(), ApprovePullRequestFailed::APPROVER_ALREADY_APPROVED)); 34 | } 35 | 36 | $events[] = new PullRequestApproved($command->pullRequestId(), $command->approver()); 37 | 38 | $approvalsRequired = 2; 39 | if (count($pullRequest->approvers()) + 1 === $approvalsRequired) { 40 | $events[] = new PullRequestMarkedAsMergeable($command->pullRequestId()); 41 | } 42 | 43 | return EventStream::fromDomainEvents(...$events); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/UseCase/AssignPullRequestReviewerCommand.php: -------------------------------------------------------------------------------- 1 | reviewer = $reviewer; 24 | $this->pullRequestId = $pullRequestId; 25 | $this->aggregateRootId = $pullRequestId; 26 | } 27 | 28 | public function reviewer(): string 29 | { 30 | return $this->reviewer; 31 | } 32 | 33 | public function pullRequestId(): string 34 | { 35 | return $this->pullRequestId; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/UseCase/AssignPullRequestReviewerCommandHandler.php: -------------------------------------------------------------------------------- 1 | reviewer()) { 24 | return EventStream::fromDomainEvents(new PullRequestReviewerAssignationFailed($command->pullRequestId(), $command->reviewer(), PullRequestReviewerAssignationFailed::EMPTY_REVIEWER)); 25 | } 26 | 27 | if (2 === count($pullRequest->assignedReviewers())) { 28 | return EventStream::fromDomainEvents(new PullRequestReviewerAssignationFailed($command->pullRequestId(), $command->reviewer(), PullRequestReviewerAssignationFailed::MAX_REVIEWERS_ASSIGNED)); 29 | } 30 | 31 | if (in_array($command->reviewer(), $pullRequest->assignedReviewers())) { 32 | return EventStream::fromDomainEvents(new PullRequestReviewerAssignationFailed($command->pullRequestId(), $command->reviewer(), PullRequestReviewerAssignationFailed::REVIEWER_ALREADY_ASSIGNED)); 33 | } 34 | 35 | return EventStream::fromDomainEvents(new PullRequestReviewerAssigned($command->pullRequestId(), $command->reviewer())); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/UseCase/CreatePullRequestCommand.php: -------------------------------------------------------------------------------- 1 | code = $code; 29 | $this->writer = $writer; 30 | $this->pullRequestId = $pullRequestId; 31 | $this->aggregateRootId = $pullRequestId; 32 | } 33 | 34 | public function code(): string 35 | { 36 | return $this->code; 37 | } 38 | 39 | public function writer(): string 40 | { 41 | return $this->writer; 42 | } 43 | 44 | public function pullRequestId(): string 45 | { 46 | return $this->pullRequestId; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/UseCase/CreatePullRequestCommandHandler.php: -------------------------------------------------------------------------------- 1 | code()) { 24 | return EventStream::fromDomainEvents(new PullRequestCreationFailed($command->writer(), $command->code(), PullRequestCreationFailed::EMPTY_CODE)); 25 | } 26 | 27 | if (!$command->writer()) { 28 | return EventStream::fromDomainEvents(new PullRequestCreationFailed($command->writer(), $command->code(), PullRequestCreationFailed::EMPTY_WRITER)); 29 | } 30 | 31 | return EventStream::fromDomainEvents(new PullRequestCreated($command->pullRequestId(), $command->writer(), $command->code())); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/UseCase/MergePullRequestCommand.php: -------------------------------------------------------------------------------- 1 | pullRequestId = $pullRequestId; 19 | $this->aggregateRootId = $pullRequestId; 20 | } 21 | 22 | public function pullRequestId(): string 23 | { 24 | return $this->pullRequestId; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /PullRequest/src/Domain/UseCase/MergePullRequestCommandHandler.php: -------------------------------------------------------------------------------- 1 | mergeable()) { 24 | return EventStream::fromDomainEvents(new MergePullRequestFailed($command->pullRequestId(), MergePullRequestFailed::NOT_MERGEABLE)); 25 | } 26 | 27 | if ($pullRequest->merged()) { 28 | return EventStream::fromDomainEvents(new MergePullRequestFailed($command->pullRequestId(), MergePullRequestFailed::ALREADY_MERGED)); 29 | } 30 | 31 | return EventStream::fromDomainEvents(new PullRequestMerged($command->pullRequestId())); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Di/ZendServiceManager/Alias/PullRequestProjectionTable.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 18 | $collection = $database->selectCollection('pull_requests'); 19 | 20 | return new ProjectionTableMongoDb($collection); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Di/ZendServiceManager/Factory/PullRequestRepositoryMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 19 | $collection = $database->selectCollection('pull_requests'); 20 | 21 | return new RepositoryMongoDb($collection, PullRequest::class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Di/ZendServiceManager/autoload/config.local.php: -------------------------------------------------------------------------------- 1 | true, 11 | ConfigAggregator::ENABLE_CACHE => false, 12 | ]; 13 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Di/ZendServiceManager/config.php: -------------------------------------------------------------------------------- 1 | 'data/cache/config-cache.php', 13 | ]; 14 | 15 | $aggregator = new ConfigAggregator([ 16 | \Zend\Expressive\Router\FastRouteRouter\ConfigProvider::class, 17 | \Zend\HttpHandlerRunner\ConfigProvider::class, 18 | Zend\ProblemDetails\ConfigProvider::class, 19 | new ArrayProvider($cacheConfig), 20 | \Zend\Expressive\Helper\ConfigProvider::class, 21 | \Zend\Expressive\ConfigProvider::class, 22 | \Zend\Expressive\Router\ConfigProvider::class, 23 | new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'), 24 | new PhpFileProvider(realpath(__DIR__) . '/development.config.php'), 25 | ], $cacheConfig['config_cache_path']); 26 | 27 | return $aggregator->getMergedConfig(); 28 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Di/ZendServiceManager/container.php: -------------------------------------------------------------------------------- 1 | configureServiceManager($container); 11 | $container->setService('config', $config); 12 | 13 | return $container; 14 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Persistence/MongoDb/ProjectionTableMongoDb.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 21 | } 22 | 23 | public function findOfId(string $id): array 24 | { 25 | $result = $this->collection->findOne(['id' => $id]); 26 | 27 | return empty($result) ? [] : $result; 28 | } 29 | 30 | public function save(string $id, array $projection): void 31 | { 32 | $projection = $this->setTime($projection); 33 | 34 | $this->collection->replaceOne(['id' => $id], $projection, ['upsert' => true]); 35 | } 36 | 37 | private function setTime(array $projection): array 38 | { 39 | $now = new UTCDateTime(); 40 | 41 | if (!isset($projection['createdAt'])) { 42 | $projection['createdAt'] = $now; 43 | } 44 | 45 | $projection['updatedAt'] = $now; 46 | 47 | return $projection; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Persistence/MongoDb/RepositoryMongoDb.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 27 | $this->stateFqcn = $stateFqcn; 28 | } 29 | 30 | public function findOfId(string $id): ?AggregateRoot 31 | { 32 | $result = $this->collection->findOne(['id' => $id]); 33 | 34 | return empty($result) ? null : hydrate($this->stateFqcn, $result); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Ui/Http/Restful/AuthorizationRules.php: -------------------------------------------------------------------------------- 1 | [ 20 | self::POST => ['writer'], 21 | ], 22 | PullRequestReviewerResource::class => [ 23 | self::PUT => ['writer'], 24 | ], 25 | PullRequestApproveResource::class => [ 26 | self::PUT => ['reviewer'], 27 | ], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Ui/Http/Restful/Resource/PullRequestApproveResource.php: -------------------------------------------------------------------------------- 1 | buildCommand($request); 26 | 27 | $result = $this->commandBus(PullRequestCommandBus::class)->handle($command); 28 | 29 | return $this->buildResponse($request, $result); 30 | } 31 | 32 | private function buildCommand(ServerRequestInterface $request): ApprovePullRequestCommand 33 | { 34 | /** @var Token $loggedUser */ 35 | $loggedUser = $request->getAttribute(Token::class); 36 | 37 | return new ApprovePullRequestCommand($request->getAttribute('id'), $loggedUser->userId()); 38 | } 39 | 40 | private function buildResponse(ServerRequestInterface $request, CommandResponse $result): ResponseInterface 41 | { 42 | $pattern = [ 43 | ApprovePullRequestFailed::class => function (ApprovePullRequestFailed $event) use ($request) { 44 | return $this->problemDetailsResponseFactory->createResponse( 45 | $request, 46 | Httpstatuscodes::HTTP_CONFLICT, 47 | $event->reason() 48 | ); 49 | }, 50 | 51 | _ => function (DomainEvent $domainEvent) use ($request) { 52 | return new EmptyResponse(Httpstatuscodes::HTTP_NO_CONTENT); 53 | }, 54 | ]; 55 | 56 | return match($pattern, $result->eventStream()->first()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Ui/Http/Restful/Resource/PullRequestReviewerResource.php: -------------------------------------------------------------------------------- 1 | buildCommand($request); 25 | 26 | $result = $this->commandBus(PullRequestCommandBus::class)->handle($command); 27 | 28 | return $this->buildResponse($request, $result); 29 | } 30 | 31 | private function buildCommand(ServerRequestInterface $request): AssignPullRequestReviewerCommand 32 | { 33 | $params = $this->getParamsFromRequest($request); 34 | 35 | return new AssignPullRequestReviewerCommand( 36 | $request->getAttribute('id'), 37 | $params->get('reviewer') 38 | ); 39 | } 40 | 41 | private function buildResponse(ServerRequestInterface $request, CommandResponse $result): ResponseInterface 42 | { 43 | $pattern = [ 44 | PullRequestReviewerAssignationFailed::class => function (PullRequestReviewerAssignationFailed $event) use ($request) { 45 | return $this->problemDetailsResponseFactory->createResponse( 46 | $request, 47 | Httpstatuscodes::HTTP_CONFLICT, 48 | $event->reason() 49 | ); 50 | }, 51 | 52 | _ => function (DomainEvent $domainEvent) use ($request) { 53 | return new EmptyResponse(Httpstatuscodes::HTTP_NO_CONTENT); 54 | }, 55 | ]; 56 | 57 | return match($pattern, $result->eventStream()->first()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Ui/Http/Restful/routes.php: -------------------------------------------------------------------------------- 1 | get('/', App\Handler\HomePageHandler::class, 'home'); 16 | * $app->post('/album', App\Handler\AlbumCreateHandler::class, 'album.create'); 17 | * $app->put('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.put'); 18 | * $app->patch('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.patch'); 19 | * $app->delete('/album/:id', App\Handler\AlbumDeleteHandler::class, 'album.delete'); 20 | * 21 | * Or with multiple request methods: 22 | * 23 | * $app->route('/contact', App\Handler\ContactHandler::class, ['GET', 'POST', ...], 'contact'); 24 | * 25 | * Or handling all request methods: 26 | * 27 | * $app->route('/contact', App\Handler\ContactHandler::class)->setName('contact'); 28 | * 29 | * or: 30 | * 31 | * $app->route( 32 | * '/contact', 33 | * App\Handler\ContactHandler::class, 34 | * Zend\Expressive\Router\Route::HTTP_METHOD_ANY, 35 | * 'contact' 36 | * ); 37 | */ 38 | return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void { 39 | $app->post('/pull-requests', PullRequestCollectionResource::class, PullRequestCollectionResource::class); 40 | $app->put('/pull-requests/{id}/reviewer', PullRequestReviewerResource::class, PullRequestReviewerResource::class); 41 | $app->put('/pull-requests/{id}/approve', PullRequestApproveResource::class, PullRequestApproveResource::class); 42 | }; 43 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Ui/Http/index.php: -------------------------------------------------------------------------------- 1 | get(\Zend\Expressive\Application::class); 22 | $factory = $container->get(\Zend\Expressive\MiddlewareFactory::class); 23 | 24 | // Execute programmatic/declarative middleware pipeline and routing 25 | // configuration statements 26 | (require __DIR__ . '/pipeline.php')($app, $factory, $container); 27 | (require __DIR__ . '/Restful/routes.php')($app, $factory, $container); 28 | 29 | $app->run(); 30 | })(); 31 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Ui/Messaging/Listener/MergePullRequestCommandListener.php: -------------------------------------------------------------------------------- 1 | body()['aggregateRootId']); 21 | 22 | /** @var CommandResponse $result */ 23 | $result = $this->commandBus(PullRequestCommandBus::class, $message)->handle($command); 24 | 25 | $pattern = [ 26 | FailureDomainEvent::class => function (FailureDomainEvent $event) { 27 | throw new \Exception($event->reason()); 28 | }, 29 | ]; 30 | 31 | match($pattern, $result->eventStream()->first()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PullRequest/src/Infrastructure/Ui/Messaging/routes.php: -------------------------------------------------------------------------------- 1 | add(, , , ); 11 | $boundedContext = $container->get('config')['service-name']; 12 | 13 | $application->addSubscription($boundedContext, 'com.pull_request.commands.merge_pull_request_command', MergePullRequestCommandListener::class); 14 | }; 15 | -------------------------------------------------------------------------------- /PullRequest/tests/PullRequest/UseCase/CreatePullRequestTest.php: -------------------------------------------------------------------------------- 1 | scenario->withCommandHandler(new CreatePullRequestCommandHandler()); 19 | $this->scenario->withProjector(new PullRequestProjector()); 20 | } 21 | 22 | /** 23 | * @test 24 | */ 25 | public function shouldCreatePullRequest() 26 | { 27 | $code = 'some code'; 28 | $writer = 'some writer'; 29 | $id = 'some id'; 30 | 31 | $this->scenario 32 | ->when((new CreatePullRequestCommand($code, $writer))->withAggregateRootId($id)) 33 | ->then(new PullRequestCreated($id, $writer, $code)) 34 | ->andProjection([ 35 | 'id' => $id, 36 | 'writer' => $writer, 37 | 'code' => $code, 38 | 'assignedReviewers' => [], 39 | 'approvers' => [], 40 | 'mergeable' => false, 41 | 'merged' => false, 42 | ]); 43 | } 44 | 45 | /** 46 | * @test 47 | */ 48 | public function shouldFail_when_emptyCode() 49 | { 50 | $code = ''; 51 | $writer = 'some writer'; 52 | $failureReason = PullRequestCreationFailed::EMPTY_CODE; 53 | 54 | $this->scenario 55 | ->when(new CreatePullRequestCommand($code, $writer)) 56 | ->then(new PullRequestCreationFailed($writer, $code, $failureReason)); 57 | } 58 | 59 | /** 60 | * @test 61 | */ 62 | public function shouldFail_when_emptyWriter() 63 | { 64 | $code = 'some code'; 65 | $writer = ''; 66 | $failureReason = PullRequestCreationFailed::EMPTY_WRITER; 67 | 68 | $this->scenario 69 | ->when(new CreatePullRequestCommand($code, $writer)) 70 | ->then(new PullRequestCreationFailed($writer, $code, $failureReason)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /PullRequest/tests/PullRequest/UseCase/MergePullRequestTest.php: -------------------------------------------------------------------------------- 1 | scenario->withCommandHandler(new MergePullRequestCommandHandler()); 20 | $this->scenario->withProjector(new PullRequestProjector()); 21 | } 22 | 23 | /** 24 | * @test 25 | */ 26 | public function shouldMerge() 27 | { 28 | $id = 'e0b5b77f-3e19-4002-b710-8a89c6c64836'; 29 | $pullRequest = (new PullRequest($id))->withMergeable(true); 30 | 31 | $this->scenario 32 | ->given($pullRequest) 33 | ->when(new MergePullRequestCommand($id)) 34 | ->then(new PullRequestMerged($id)) 35 | ->andProjection([ 36 | 'merged' => true, 37 | ]); 38 | } 39 | 40 | /** 41 | * @test 42 | */ 43 | public function shouldFail_when_pullRequestNotMergeable() 44 | { 45 | $id = 'e0b5b77f-3e19-4002-b710-8a89c6c64836'; 46 | $pullRequest = (new PullRequest($id))->withMergeable(false); 47 | $failureReason = MergePullRequestFailed::NOT_MERGEABLE; 48 | 49 | $this->scenario 50 | ->given($pullRequest) 51 | ->when(new MergePullRequestCommand($id)) 52 | ->then(new MergePullRequestFailed($id, $failureReason)); 53 | } 54 | 55 | /** 56 | * @test 57 | */ 58 | public function shouldFail_when_pullRequestAlreadyMerged() 59 | { 60 | $id = 'e0b5b77f-3e19-4002-b710-8a89c6c64836'; 61 | $pullRequest = (new PullRequest($id))->withMergeable(true)->withMerged(true); 62 | $failureReason = MergePullRequestFailed::ALREADY_MERGED; 63 | 64 | $this->scenario 65 | ->given($pullRequest) 66 | ->when(new MergePullRequestCommand($id)) 67 | ->then(new MergePullRequestFailed($id, $failureReason)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /PullRequest/var/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soa-php/code-review-app/4c11a84f745eb9257d30b6b56860e622c4918183/PullRequest/var/.gitkeep -------------------------------------------------------------------------------- /UserIdentity/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mgonzalezbaile/user-identity", 3 | "description": "User Identity Bounded Context", 4 | "license": "MIT", 5 | "require": { 6 | "php": "^7.2", 7 | "mgonzalezbaile/common": "dev-user-identity-bc", 8 | "martinezdelariva/functional": "dev-master" 9 | }, 10 | "require-dev": { 11 | "friendsofphp/php-cs-fixer": "^2.12", 12 | "phpunit/phpunit": "^7.4" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "UserIdentity\\": "src/" 17 | } 18 | }, 19 | "autoload-dev": { 20 | "psr-4": { 21 | "UserIdentityTest\\": "tests/" 22 | } 23 | }, 24 | "repositories": [ 25 | { 26 | "type": "path", 27 | "url": "../Common", 28 | "options": { 29 | "symlink": false 30 | } 31 | } 32 | ], 33 | "config": { 34 | "bin-dir": "bin/" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /UserIdentity/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | COMMAND=${1:-} 4 | shift 5 | 6 | run_in_docker="docker run -it --rm -v $PWD/..:/srv/app -w /srv/app/UserIdentity --user ${UID} mgonzalezbaile/php_base:1.0" 7 | 8 | case "$COMMAND" in 9 | composer) 10 | ${run_in_docker} composer $@ 11 | ;; 12 | phpunit) 13 | ${run_in_docker} vendor/phpunit/phpunit/phpunit tests 14 | ;; 15 | esac 16 | -------------------------------------------------------------------------------- /UserIdentity/src/Application/Projection/UserProjector.php: -------------------------------------------------------------------------------- 1 | streamId(); 18 | $projection['username'] = $event->username(); 19 | $projection['email'] = $event->email(); 20 | $projection['password'] = $event->password(); 21 | $projection['accessToken'] = $event->accessToken(); 22 | $projection['refreshToken'] = $event->refreshToken(); 23 | $projection['roles'] = $event->roles(); 24 | 25 | return $projection; 26 | } 27 | 28 | public function projectLogUserInWithPasswordFailed(LogUserInWithPasswordFailed $event, array $projection): array 29 | { 30 | return $projection; 31 | } 32 | 33 | public function projectUserAccessTokenRefreshed(UserAccessTokenRefreshed $event, array $projection): array 34 | { 35 | $projection['accessToken'] = $event->accessToken(); 36 | 37 | return $projection; 38 | } 39 | 40 | public function projectRefreshUserAccessTokenFailed(RefreshUserAccessTokenFailed $event, array $projection): array 41 | { 42 | return $projection; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/AllowedRoles.php: -------------------------------------------------------------------------------- 1 | wasSucceed = $wasSucceed; 32 | $this->failureReason = $failureReason; 33 | } 34 | 35 | public function wasSucceed(): bool 36 | { 37 | return $this->wasSucceed; 38 | } 39 | 40 | public function failureReason(): string 41 | { 42 | return $this->failureReason; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/Event/LogUserInWithPasswordFailed.php: -------------------------------------------------------------------------------- 1 | id = $id; 30 | $this->reason = $reason; 31 | } 32 | 33 | public function reason(): string 34 | { 35 | return $this->reason; 36 | } 37 | 38 | public function streamId(): string 39 | { 40 | return $this->id; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/Event/RefreshUserAccessTokenFailed.php: -------------------------------------------------------------------------------- 1 | id = $id; 30 | $this->reason = $reason; 31 | } 32 | 33 | public function reason(): string 34 | { 35 | return $this->reason; 36 | } 37 | 38 | public function streamId(): string 39 | { 40 | return $this->id; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/Event/UserAccessTokenRefreshed.php: -------------------------------------------------------------------------------- 1 | userId = $userId; 24 | $this->accessToken = $accessToken; 25 | } 26 | 27 | public function streamId(): string 28 | { 29 | return $this->userId; 30 | } 31 | 32 | public function accessToken(): string 33 | { 34 | return $this->accessToken; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/Event/UserWithPasswordLoggedIn.php: -------------------------------------------------------------------------------- 1 | id = $id; 56 | $this->username = $username; 57 | $this->password = $password; 58 | $this->email = $email; 59 | $this->roles = $roles; 60 | $this->accessToken = $accessToken; 61 | $this->refreshToken = $refreshToken; 62 | } 63 | 64 | public function streamId(): string 65 | { 66 | return $this->id; 67 | } 68 | 69 | public function username(): string 70 | { 71 | return $this->username; 72 | } 73 | 74 | public function password(): string 75 | { 76 | return $this->password; 77 | } 78 | 79 | public function email(): string 80 | { 81 | return $this->email; 82 | } 83 | 84 | public function roles(): array 85 | { 86 | return $this->roles; 87 | } 88 | 89 | public function accessToken(): string 90 | { 91 | return $this->accessToken; 92 | } 93 | 94 | public function refreshToken(): string 95 | { 96 | return $this->refreshToken; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/PasswordEncryption.php: -------------------------------------------------------------------------------- 1 | username = $username; 39 | $this->password = $password; 40 | $this->email = $email; 41 | $this->roles = $roles; 42 | $this->userId = $userId; 43 | $this->aggregateRootId = $userId; 44 | } 45 | 46 | public function username(): string 47 | { 48 | return $this->username; 49 | } 50 | 51 | public function password(): string 52 | { 53 | return $this->password; 54 | } 55 | 56 | public function email(): string 57 | { 58 | return $this->email; 59 | } 60 | 61 | public function roles(): array 62 | { 63 | return $this->roles; 64 | } 65 | 66 | public function userId(): string 67 | { 68 | return $this->userId; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/UseCase/LogUserInWithPasswordCommandHandler.php: -------------------------------------------------------------------------------- 1 | tokenBuilder = $tokenBuilder; 40 | $this->passwordEncryption = $passwordEncryption; 41 | $this->contentValidator = $contentValidator; 42 | } 43 | 44 | /** 45 | * @param LogUserInWithPasswordCommand $command 46 | */ 47 | public function handle(Command $command, AggregateRoot $user = null): EventStream 48 | { 49 | $validationResult = $this->contentValidator->validate($command); 50 | 51 | if (!$validationResult->wasSucceed()) { 52 | return EventStream::fromDomainEvents( 53 | LogUserInWithPasswordFailed::withReason($command->userId(), $validationResult->failureReason()) 54 | ); 55 | } 56 | 57 | return EventStream::fromDomainEvents(new UserWithPasswordLoggedIn( 58 | $command->userId(), 59 | $command->username(), 60 | $this->passwordEncryption->encrypt($command->password()), 61 | $command->email(), 62 | $command->roles(), 63 | $this->tokenBuilder->createAccessToken($command->userId(), $command->roles()), 64 | $this->tokenBuilder->createRefreshToken($command->userId(), $command->roles()) 65 | )); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/UseCase/RefreshUserAccessTokenCommand.php: -------------------------------------------------------------------------------- 1 | userId = $userId; 19 | $this->aggregateRootId = $userId; 20 | } 21 | 22 | public function userId(): string 23 | { 24 | return $this->userId; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/UseCase/RefreshUserAccessTokenCommandHandler.php: -------------------------------------------------------------------------------- 1 | tokenFactory = $tokenFactory; 32 | $this->tokenValidator = $tokenValidator; 33 | } 34 | 35 | /** 36 | * @param User $user 37 | * @param RefreshUserAccessTokenCommand $command 38 | */ 39 | public function handle(Command $command, AggregateRoot $user): EventStream 40 | { 41 | if (!$this->tokenValidator->isValid($user->refreshToken())) { 42 | return EventStream::fromDomainEvents(RefreshUserAccessTokenFailed::becauseGivenRefreshTokenIsInvalid($user->id(), $user->refreshToken())); 43 | } 44 | 45 | return EventStream::fromDomainEvents( 46 | new UserAccessTokenRefreshed($user->id(), $this->tokenFactory->createAccessToken($user->id(), $user->roles())) 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/User.php: -------------------------------------------------------------------------------- 1 | id = $id; 29 | $this->refreshToken = $refreshToken; 30 | $this->roles = $roles; 31 | } 32 | 33 | public function id(): string 34 | { 35 | return $this->id; 36 | } 37 | 38 | public function refreshToken(): string 39 | { 40 | return $this->refreshToken; 41 | } 42 | 43 | public function roles(): array 44 | { 45 | return $this->roles; 46 | } 47 | 48 | public function withRefreshToken(string $refreshToken): self 49 | { 50 | $clone = clone $this; 51 | $clone->refreshToken = $refreshToken; 52 | 53 | return $clone; 54 | } 55 | 56 | public function withRoles(array $roles): self 57 | { 58 | $clone = clone $this; 59 | $clone->roles = $roles; 60 | 61 | return $clone; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /UserIdentity/src/Domain/UserWithPasswordContentValidator.php: -------------------------------------------------------------------------------- 1 | roles() as $role) { 17 | Assert::that($role)->inArray(AllowedRoles::$roles); 18 | } 19 | 20 | Assert::lazy() 21 | ->that($command->email(), 'email')->email() 22 | ->that($command->username(), 'username')->notEmpty() 23 | ->that($command->password(), 'password')->notEmpty() 24 | ->verifyNow(); 25 | } catch (\InvalidArgumentException | LazyAssertionException $exception) { 26 | return ContentValidationResult::failed($exception->getMessage()); 27 | } 28 | 29 | return ContentValidationResult::succeed(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Di/ZendServiceManager/Alias/UserProjectionTable.php: -------------------------------------------------------------------------------- 1 | get('config')['jwt'], new Sha256(), new ClockImpl()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Di/ZendServiceManager/Factory/JwtTokenValidatorFactory.php: -------------------------------------------------------------------------------- 1 | get('config')['jwt'], new Sha256()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Di/ZendServiceManager/Factory/UseCase/RefreshUserAccessTokenCommandHandlerFactory.php: -------------------------------------------------------------------------------- 1 | get(TokenValidator::class), 18 | $container->get(TokenFactory::class) 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Di/ZendServiceManager/Factory/UseCase/RegisterUserWithPasswordCommandHandlerFactory.php: -------------------------------------------------------------------------------- 1 | get(TokenFactory::class), 19 | $container->get(PasswordEncryption::class), 20 | new UserWithPasswordContentValidator() 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Di/ZendServiceManager/Factory/UserProjectionTableMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 18 | $collection = $database->selectCollection('users'); 19 | 20 | return new ProjectionTableMongoDb($collection); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Di/ZendServiceManager/Factory/UserRepositoryMongoDbFactory.php: -------------------------------------------------------------------------------- 1 | get(Database::class); 19 | $collection = $database->selectCollection('users'); 20 | 21 | return new RepositoryMongoDb($collection, User::class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Di/ZendServiceManager/autoload/config.local.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'factories' => [ 12 | ErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class, 13 | 'Zend\Expressive\Whoops' => Container\WhoopsFactory::class, 14 | 'Zend\Expressive\WhoopsPageHandler' => Container\WhoopsPageHandlerFactory::class, 15 | ], 16 | ], 17 | 'whoops' => [ 18 | 'json_exceptions' => [ 19 | 'display' => true, 20 | 'show_trace' => true, 21 | 'ajax_only' => true, 22 | ], 23 | ], 24 | 'debug' => true, 25 | ConfigAggregator::ENABLE_CACHE => false, 26 | ]; 27 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Di/ZendServiceManager/config.php: -------------------------------------------------------------------------------- 1 | 'data/cache/config-cache.php', 13 | ]; 14 | 15 | $aggregator = new ConfigAggregator([ 16 | \Zend\Expressive\Router\FastRouteRouter\ConfigProvider::class, 17 | \Zend\HttpHandlerRunner\ConfigProvider::class, 18 | Zend\ProblemDetails\ConfigProvider::class, 19 | new ArrayProvider($cacheConfig), 20 | \Zend\Expressive\Helper\ConfigProvider::class, 21 | \Zend\Expressive\ConfigProvider::class, 22 | \Zend\Expressive\Router\ConfigProvider::class, 23 | new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'), 24 | new PhpFileProvider(realpath(__DIR__) . '/development.config.php'), 25 | ], $cacheConfig['config_cache_path']); 26 | 27 | return $aggregator->getMergedConfig(); 28 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Di/ZendServiceManager/container.php: -------------------------------------------------------------------------------- 1 | configureServiceManager($container); 11 | $container->setService('config', $config); 12 | 13 | return $container; 14 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Domain/BCryptPasswordEncryption.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 21 | } 22 | 23 | public function findOfId(string $id): array 24 | { 25 | $result = $this->collection->findOne(['id' => $id]); 26 | 27 | return empty($result) ? [] : $result; 28 | } 29 | 30 | public function save(string $id, array $projection): void 31 | { 32 | $projection = $this->setTime($projection); 33 | 34 | $this->collection->replaceOne(['id' => $id], $projection, ['upsert' => true]); 35 | } 36 | 37 | private function setTime(array $projection): array 38 | { 39 | $now = new UTCDateTime(); 40 | 41 | if (!isset($projection['createdAt'])) { 42 | $projection['createdAt'] = $now; 43 | } 44 | 45 | $projection['updatedAt'] = $now; 46 | 47 | return $projection; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Persistence/MongoDb/RepositoryMongoDb.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 27 | $this->stateFqcn = $stateFqcn; 28 | } 29 | 30 | public function findOfId(string $id): ?AggregateRoot 31 | { 32 | $result = $this->collection->findOne(['id' => $id]); 33 | 34 | return empty($result) ? null : hydrate($this->stateFqcn, $result); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Ui/Http/Restful/AuthorizationRules.php: -------------------------------------------------------------------------------- 1 | [ 17 | AuthorizationType::PUT_METHOD => ['writer', 'reviewer'], 18 | ], 19 | UserWithPasswordCollectionResource::class => [ 20 | AuthorizationType::POST_METHOD => AuthorizationType::NO_AUTH, 21 | ], 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Ui/Http/Restful/Resource/UserAccessTokenResource.php: -------------------------------------------------------------------------------- 1 | buildCommand($request); 24 | 25 | $result = $this->commandBus(UserIdentityCommandBus::class)->handle($command); 26 | 27 | return $this->buildResponse($request, $result); 28 | } 29 | 30 | private function buildCommand(ServerRequestInterface $request): RefreshUserAccessTokenCommand 31 | { 32 | return new RefreshUserAccessTokenCommand($request->getAttribute('id')); 33 | } 34 | 35 | private function buildResponse(ServerRequestInterface $request, CommandResponse $result): ResponseInterface 36 | { 37 | $pattern = [ 38 | RefreshUserAccessTokenFailed::class => function (RefreshUserAccessTokenFailed $event) use ($request) { 39 | return $this->problemDetailsResponseFactory->createResponse( 40 | $request, 41 | Httpstatuscodes::HTTP_CONFLICT, 42 | $event->reason() 43 | ); 44 | }, 45 | 46 | UserAccessTokenRefreshed::class => function (UserAccessTokenRefreshed $domainEvent) use ($request) { 47 | $responseContent = [ 48 | 'access-token' => $domainEvent->accessToken(), 49 | ]; 50 | 51 | return (new JsonResponse($responseContent)) 52 | ->withStatus(Httpstatuscodes::HTTP_OK); 53 | }, 54 | ]; 55 | 56 | return match($pattern, $result->eventStream()->first()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Ui/Http/Restful/routes.php: -------------------------------------------------------------------------------- 1 | get('/', App\Handler\HomePageHandler::class, 'home'); 15 | * $app->post('/album', App\Handler\AlbumCreateHandler::class, 'album.create'); 16 | * $app->put('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.put'); 17 | * $app->patch('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.patch'); 18 | * $app->delete('/album/:id', App\Handler\AlbumDeleteHandler::class, 'album.delete'); 19 | * 20 | * Or with multiple request methods: 21 | * 22 | * $app->route('/contact', App\Handler\ContactHandler::class, ['GET', 'POST', ...], 'contact'); 23 | * 24 | * Or handling all request methods: 25 | * 26 | * $app->route('/contact', App\Handler\ContactHandler::class)->setName('contact'); 27 | * 28 | * or: 29 | * 30 | * $app->route( 31 | * '/contact', 32 | * App\Handler\ContactHandler::class, 33 | * Zend\Expressive\Router\Route::HTTP_METHOD_ANY, 34 | * 'contact' 35 | * ); 36 | */ 37 | return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container): void { 38 | $app->post( 39 | '/users/login/password', 40 | UserWithPasswordCollectionResource::class, 41 | UserWithPasswordCollectionResource::class 42 | ); 43 | 44 | $app->put( 45 | '/users/{id}/access-token', 46 | UserAccessTokenResource::class, 47 | UserAccessTokenResource::class 48 | ); 49 | 50 | // $app->post( 51 | // '/users/login/google', 52 | // UserWithPasswordCollectionResource::class, 53 | // UserWithPasswordCollectionResource::class 54 | // ); 55 | // 56 | // $app->post( 57 | // '/users/login/facebook', 58 | // UserWithPasswordCollectionResource::class, 59 | // UserWithPasswordCollectionResource::class 60 | // ); 61 | }; 62 | -------------------------------------------------------------------------------- /UserIdentity/src/Infrastructure/Ui/Http/index.php: -------------------------------------------------------------------------------- 1 | get(\Zend\Expressive\Application::class); 22 | $factory = $container->get(\Zend\Expressive\MiddlewareFactory::class); 23 | 24 | // Execute programmatic/declarative middleware pipeline and routing 25 | // configuration statements 26 | (require __DIR__ . '/pipeline.php')($app, $factory, $container); 27 | (require __DIR__ . '/Restful/routes.php')($app, $factory, $container); 28 | 29 | $app->run(); 30 | })(); 31 | -------------------------------------------------------------------------------- /UserIdentity/tests/UserIdentity/Double/PasswordEncryptionStub.php: -------------------------------------------------------------------------------- 1 | encryptedPassword = $encryptedPassword; 24 | $this->isValid = $isValid; 25 | } 26 | 27 | public function encrypt(string $password): string 28 | { 29 | return $this->encryptedPassword; 30 | } 31 | 32 | public function isValid(string $encryptedPassword, string $plainPassword): bool 33 | { 34 | return $this->isValid; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /UserIdentity/tests/UserIdentity/Double/TokenFactoryStub.php: -------------------------------------------------------------------------------- 1 | accessToken = $accessToken; 29 | $this->refreshToken = $refreshToken; 30 | } 31 | 32 | public function createAccessToken(string $userId, array $userRoles): string 33 | { 34 | return $this->accessToken; 35 | } 36 | 37 | public function createRefreshToken(string $userId, array $userRoles): string 38 | { 39 | return $this->refreshToken; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /UserIdentity/tests/UserIdentity/Double/TokenValidatorStub.php: -------------------------------------------------------------------------------- 1 | isTokenValid = $isTokenValid; 29 | } 30 | 31 | public function isValid(string $token): bool 32 | { 33 | return $this->isTokenValid; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /UserIdentity/tests/UserIdentity/Double/UserWithPasswordContentValidatorStub.php: -------------------------------------------------------------------------------- 1 | result = $result; 21 | } 22 | 23 | public function validate(LogUserInWithPasswordCommand $command): ContentValidationResult 24 | { 25 | return $this->result; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docker/.php_cs.dist: -------------------------------------------------------------------------------- 1 | in(__DIR__); 5 | 6 | return PhpCsFixer\Config::create() 7 | ->setRiskyAllowed(true) 8 | ->setRules( 9 | [ 10 | '@Symfony' => true, 11 | 'array_syntax' => ['syntax' => 'short'], 12 | 'concat_space' => ['spacing' => 'one'], 13 | 'declare_strict_types' => true, 14 | 'linebreak_after_opening_tag' => true, 15 | 'phpdoc_order' => true, 16 | 'binary_operator_spaces' => [ 17 | 'align_equals' => true, 18 | 'align_double_arrow' => true, 19 | ], 20 | ] 21 | ) 22 | ->setFinder($finder); 23 | -------------------------------------------------------------------------------- /docker/Dockerfile.nginx: -------------------------------------------------------------------------------- 1 | FROM jwilder/nginx-proxy 2 | 3 | COPY default.conf /etc/nginx/conf.d/default.conf 4 | -------------------------------------------------------------------------------- /docker/default.conf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soa-php/code-review-app/4c11a84f745eb9257d30b6b56860e622c4918183/docker/default.conf -------------------------------------------------------------------------------- /docker/infra-docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mongo: 4 | image: mongo 5 | container_name: mongo 6 | ports: 7 | - "27017:27017" 8 | networks: 9 | - common 10 | 11 | rabbitmq: 12 | image: rabbitmq:3.6-management 13 | container_name: rabbitmq 14 | ports: 15 | - "5672:5672" 16 | - "15672:15672" 17 | environment: 18 | - RABBITMQ_DEFAULT_USER=devuser 19 | - RABBITMQ_DEFAULT_PASS=devpass 20 | - RABBITMQ_DEFAULT_VHOST=devhost 21 | networks: 22 | - common 23 | 24 | networks: 25 | common: 26 | external: true 27 | -------------------------------------------------------------------------------- /docker/lint_code.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | FILES=$(find . -not -path "./vendor/*" | grep "\.php$") 4 | php-cs-fixer fix --config=/opt/php_cs/.php_cs.dist --diff --using-cache=no $FILES 5 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | 8 | events { 9 | worker_connections 1024; 10 | } 11 | 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 18 | '$status $body_bytes_sent "$http_referer" ' 19 | '"$http_user_agent" "$http_x_forwarded_for"'; 20 | 21 | access_log /var/log/nginx/access.log main; 22 | 23 | sendfile on; 24 | 25 | keepalive_timeout 65; 26 | 27 | } 28 | 29 | include /etc/nginx/conf.d/*.conf; 30 | 31 | daemon off; -------------------------------------------------------------------------------- /docker/services-docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | #SERVERS 4 | pull_request_server: 5 | image: mgonzalezbaile/php_base:1.0 6 | container_name: pull_request_server 7 | volumes: 8 | - ..:/srv/app 9 | working_dir: /srv/app/PullRequest 10 | ports: 11 | - "8080:8080" 12 | command: 'php -S 0.0.0.0:8080 /srv/app/PullRequest/src/Infrastructure/Ui/Http/index.php' 13 | networks: 14 | - common 15 | 16 | user_identity_server: 17 | image: mgonzalezbaile/php_base:1.0 18 | container_name: user_identity_server 19 | volumes: 20 | - ..:/srv/app 21 | working_dir: /srv/app/UserIdentity 22 | ports: 23 | - "8081:8080" 24 | command: 'php -S 0.0.0.0:8080 /srv/app/UserIdentity/src/Infrastructure/Ui/Http/index.php' 25 | networks: 26 | - common 27 | 28 | #LISTENERS 29 | pull_request_message_listener: 30 | image: mgonzalezbaile/php_base:1.0 31 | volumes: 32 | - ..:/srv/app 33 | working_dir: /srv/app/PullRequest 34 | command: '/srv/app/docker/wait-for-it.sh rabbitmq:15672 -- php bin/listen-messages' 35 | networks: 36 | - common 37 | 38 | merge_pull_request_pm_message_listener: 39 | image: mgonzalezbaile/php_base:1.0 40 | volumes: 41 | - ..:/srv/app 42 | working_dir: /srv/app/MergePullRequestPm 43 | command: '/srv/app/docker/wait-for-it.sh rabbitmq:15672 -- php bin/listen-messages' 44 | networks: 45 | - common 46 | 47 | payment_message_listener: 48 | image: mgonzalezbaile/php_base:1.0 49 | volumes: 50 | - ..:/srv/app 51 | working_dir: /srv/app/Payment 52 | command: '/srv/app/docker/wait-for-it.sh rabbitmq:15672 -- php bin/listen-messages' 53 | networks: 54 | - common 55 | 56 | # Publisher 57 | message_publisher: 58 | image: mgonzalezbaile/php_base:1.0 59 | volumes: 60 | - ..:/srv/app 61 | working_dir: /srv/app/MessagePublisher 62 | command: '/srv/app/docker/wait-for-it.sh rabbitmq:15672 -- php bin/publish-messages' 63 | networks: 64 | - common 65 | 66 | 67 | networks: 68 | common: 69 | external: true 70 | -------------------------------------------------------------------------------- /docker/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TIMEOUT=15 4 | QUIET=0 5 | 6 | echoerr() { 7 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi 8 | } 9 | 10 | usage() { 11 | exitcode="$1" 12 | cat << USAGE >&2 13 | Usage: 14 | $cmdname host:port [-t timeout] [-- command args] 15 | -q | --quiet Do not output any status messages 16 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout 17 | -- COMMAND ARGS Execute command with args after the test finishes 18 | USAGE 19 | exit "$exitcode" 20 | } 21 | 22 | wait_for() { 23 | for i in `seq $TIMEOUT` ; do 24 | nc -z "$HOST" "$PORT" > /dev/null 2>&1 25 | 26 | result=$? 27 | if [ $result -eq 0 ] ; then 28 | if [ $# -gt 0 ] ; then 29 | exec "$@" 30 | fi 31 | exit 0 32 | fi 33 | sleep 1 34 | done 35 | echo "Operation timed out" >&2 36 | exit 1 37 | } 38 | 39 | while [ $# -gt 0 ] 40 | do 41 | case "$1" in 42 | *:* ) 43 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1) 44 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2) 45 | shift 1 46 | ;; 47 | -q | --quiet) 48 | QUIET=1 49 | shift 1 50 | ;; 51 | -t) 52 | TIMEOUT="$2" 53 | if [ "$TIMEOUT" = "" ]; then break; fi 54 | shift 2 55 | ;; 56 | --timeout=*) 57 | TIMEOUT="${1#*=}" 58 | shift 1 59 | ;; 60 | --) 61 | shift 62 | break 63 | ;; 64 | --help) 65 | usage 0 66 | ;; 67 | *) 68 | echoerr "Unknown argument: $1" 69 | usage 1 70 | ;; 71 | esac 72 | done 73 | 74 | if [ "$HOST" = "" -o "$PORT" = "" ]; then 75 | echoerr "Error: you need to provide a host and port to test." 76 | usage 2 77 | fi 78 | 79 | wait_for "$@" -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd MergePullRequestPm 4 | ./run.sh composer install 5 | 6 | cd ../Payment/ 7 | ./run.sh composer install 8 | 9 | cd ../PullRequest 10 | ./run.sh composer install 11 | 12 | cd ../Common 13 | ./run.sh composer install 14 | 15 | cd ../UserIdentity 16 | ./run.sh composer install 17 | 18 | cd ../MessagePublisher 19 | ./run.sh composer install 20 | 21 | docker network inspect common &>/dev/null || docker network create common --------------------------------------------------------------------------------