├── var ├── .gitkeep └── session │ └── .gitkeep ├── .gitignore ├── .env.end_to_end_testing ├── bin ├── php ├── composer ├── restart ├── console ├── fix ├── test ├── cleanup ├── load-users └── install ├── docker ├── php-fpm │ ├── Dockerfile │ └── php.ini ├── nginx │ ├── Dockerfile │ └── template.conf ├── php │ ├── php.ini │ └── Dockerfile └── web │ ├── php.ini │ └── Dockerfile ├── public ├── img │ └── logo.png ├── .htaccess └── index.php ├── src ├── App │ ├── ExternalEvents │ │ ├── ConsumerRestarted.php │ │ ├── ExternalEventPublisher.php │ │ ├── ExternalEventConsumer.php │ │ ├── AsynchronousExternalEventPublisher.php │ │ ├── ExternalEventConsumersFactory.php │ │ ├── SynchronousExternalEventPublisher.php │ │ ├── PublishExternalEvent.php │ │ └── EventStreamConfigProvider.php │ ├── EventDispatcher.php │ ├── Entity │ │ ├── UserRepository.php │ │ ├── CouldNotFindUser.php │ │ ├── EventRecordingCapabilities.php │ │ ├── UserId.php │ │ ├── UserHasSignedUp.php │ │ ├── UserType.php │ │ ├── UserRepositoryUsingDbal.php │ │ └── User.php │ ├── ApplicationInterface.php │ ├── Json.php │ ├── Cli │ │ ├── ExportUsersCommand.php │ │ ├── ConsoleApplication.php │ │ ├── OutboxRelayCommand.php │ │ ├── ConsumeEventsCommand.php │ │ └── SignUpCommand.php │ ├── Twig │ │ └── SessionExtension.php │ ├── AddFlashMessage.php │ ├── Mapping.php │ ├── Handler │ │ ├── LogoutHandler.php │ │ ├── SwitchUserHandler.php │ │ ├── LoginHandler.php │ │ └── SignUpHandler.php │ ├── ConnectionFactory.php │ ├── EventDispatcherFactory.php │ ├── ConfigurableEventDispatcher.php │ ├── Application.php │ ├── Session.php │ ├── SchemaManager.php │ └── ConfigProvider.php ├── MeetupOrganizing │ ├── Entity │ │ ├── Answer.php │ │ ├── RsvpWasCancelled.php │ │ ├── CouldNotFindMeetup.php │ │ ├── RsvpRepository.php │ │ ├── CouldNotFindRsvp.php │ │ ├── MeetupId.php │ │ ├── RsvpId.php │ │ ├── UserHasRsvpd.php │ │ └── Rsvp.php │ ├── ViewModel │ │ ├── Organizer.php │ │ ├── MeetupDetails.php │ │ └── MeetupDetailsRepository.php │ ├── Application │ │ ├── RsvpForMeetup.php │ │ └── SignUp.php │ ├── Handler │ │ ├── ApiPingHandler.php │ │ ├── ApiCountMeetupsHandler.php │ │ ├── MeetupDetailsHandler.php │ │ ├── CancelRsvpHandler.php │ │ ├── ListMeetupsHandler.php │ │ ├── RsvpForMeetupHandler.php │ │ ├── CancelMeetupHandler.php │ │ ├── ScheduleMeetupHandler.php │ │ └── RescheduleMeetupHandler.php │ └── Infrastructure │ │ └── RsvpRepositoryUsingDbal.php └── Billing │ ├── ViewModel │ ├── Organizer.php │ └── Invoice.php │ ├── Projections │ └── OrganizerProjection.php │ └── Handler │ ├── DeleteInvoiceHandler.php │ ├── ListOrganizersHandler.php │ ├── ListInvoicesHandler.php │ └── CreateInvoiceHandler.php ├── .editorconfig ├── config ├── container.php ├── end_to_end_testing.php ├── application_testing.php ├── autoload │ ├── mezzio.global.php │ └── dependencies.global.php ├── config.php ├── routes.php └── pipeline.php ├── phpstan.neon ├── templates ├── layout │ ├── _flashes.html.twig │ ├── default.html.twig │ └── _navigation.html.twig ├── error │ ├── 404.html.twig │ └── error.html.twig ├── app │ ├── login.html.twig │ ├── reschedule-meetup.html.twig │ ├── list-meetups.html.twig │ ├── meetup-details.html.twig │ ├── sign-up.html.twig │ └── schedule-meetup.html.twig ├── admin │ └── list-organizers.html.twig └── billing │ ├── list-invoices.html.twig │ └── create-invoice.html.twig ├── test ├── JsonTest.php └── AppTest │ ├── PageObject │ ├── LoginPage.php │ ├── ListOrganizersPage.php │ ├── CreateInvoicePage.php │ ├── GenericPage.php │ ├── MeetupSnippet.php │ ├── RescheduleMeetupPage.php │ ├── SignUpPage.php │ ├── OrganizerSnippet.php │ ├── ListInvoicesPage.php │ ├── ListMeetupsPage.php │ ├── AbstractPageObject.php │ ├── MeetupDetailsPage.php │ └── ScheduleMeetupPage.php │ ├── ApiTest.php │ ├── CancelMeetupTest.php │ ├── RescheduleMeetupTest.php │ ├── AbstractApplicationTest.php │ ├── ApplicationLevelInvoicingTest.php │ ├── SuccessfulResponse.php │ ├── UnsuccessfulResponse.php │ ├── InvoicingTest.php │ ├── RsvpForMeetupTest.php │ ├── ScheduleMeetupTest.php │ ├── SignUpCommandTest.php │ └── AbstractBrowserTest.php ├── console.php ├── phpunit.xml.dist ├── .github └── workflows │ └── code_analysis.yaml ├── rector.php ├── LICENSE ├── ecs.php ├── phpstan-baseline.neon ├── README.md ├── composer.json └── docker-compose.yml /var/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /var/session/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.env 3 | /var/ 4 | /.idea 5 | -------------------------------------------------------------------------------- /.env.end_to_end_testing: -------------------------------------------------------------------------------- 1 | APPLICATION_ENV=end_to_end_testing 2 | -------------------------------------------------------------------------------- /bin/php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker compose run --rm php php "$@" 4 | -------------------------------------------------------------------------------- /bin/composer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker compose run --rm composer "$@" 4 | -------------------------------------------------------------------------------- /bin/restart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker compose up -d --force-recreate 4 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker compose run --rm php php console.php "$@" 4 | -------------------------------------------------------------------------------- /docker/php-fpm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-fpm-alpine 2 | COPY php.ini /usr/local/etc/php/ 3 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.19-alpine 2 | COPY template.conf /etc/nginx/templates/default.conf.template 3 | -------------------------------------------------------------------------------- /docker/php/php.ini: -------------------------------------------------------------------------------- 1 | date.timezone = "UTC" 2 | error_reporting = E_ALL 3 | log_errors = "1" 4 | memory_limit = 2G 5 | -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthiasnoback/hexagonal-architecture-workshop/HEAD/public/img/logo.png -------------------------------------------------------------------------------- /docker/php-fpm/php.ini: -------------------------------------------------------------------------------- 1 | display_errors = On 2 | html_errors= Off 3 | error_reporting = E_ALL 4 | date.timezone = "UTC" 5 | session.save_path = "/app/var/session" 6 | -------------------------------------------------------------------------------- /src/App/ExternalEvents/ConsumerRestarted.php: -------------------------------------------------------------------------------- 1 | 3 | {% for message in flashes %} 4 |
7 | {% endfor %} 8 | 9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /src/App/EventDispatcher.php: -------------------------------------------------------------------------------- 1 | $events 11 | */ 12 | public function dispatchAll(array $events): void; 13 | 14 | public function dispatch(object $event): void; 15 | } 16 | -------------------------------------------------------------------------------- /src/App/ExternalEvents/ExternalEventConsumer.php: -------------------------------------------------------------------------------- 1 | 'bar', 14 | ]; 15 | 16 | self::assertSame($data, Json::decode(Json::encode($data))); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/MeetupOrganizing/Entity/RsvpRepository.php: -------------------------------------------------------------------------------- 1 | get(ConsoleApplication::class); 15 | 16 | $application->run(); 17 | })(); 18 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 |We encountered a 404 Not Found error.
9 |10 | You are looking for something that doesn't exist or may have moved. Check out one of the links on this page 11 | or head back to Home. 12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /bin/install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | echo "Creating .env file" 6 | printf "HOST_UID=%s\nHOST_GID=%s\n" "$(id -u)" "$(id -g)" > .env 7 | 8 | echo "Pulling Docker images" 9 | docker compose pull 10 | 11 | echo "Installing Composer dependencies" 12 | docker compose run --rm composer install --ignore-platform-reqs 13 | 14 | echo "Starting all services in docker compose.yml" 15 | docker compose up -d 16 | 17 | echo "" 18 | echo "Now open http://localhost:8000/ in your browser" 19 | echo "" 20 | -------------------------------------------------------------------------------- /test/AppTest/PageObject/LoginPage.php: -------------------------------------------------------------------------------- 1 | submitForm('Log in', [ 14 | 'emailAddress' => $emailAddress, 15 | ]); 16 | 17 | self::assertSuccessfulResponse($browser); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/MeetupOrganizing/ViewModel/Organizer.php: -------------------------------------------------------------------------------- 1 | id; 18 | } 19 | 20 | public function name(): string 21 | { 22 | return $this->name; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/end_to_end_testing.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'factories' => [ 12 | ExternalEventPublisher::class => fn (ContainerInterface $container) => new SynchronousExternalEventPublisher( 13 | $container->get('external_event_consumers') 14 | ), 15 | ], 16 | ], 17 | ]; 18 | -------------------------------------------------------------------------------- /test/AppTest/PageObject/ListOrganizersPage.php: -------------------------------------------------------------------------------- 1 | crawler->filter('.organizer'); 12 | 13 | if (count($organizers) === 0) { 14 | throw new \RuntimeException('No organizers found'); 15 | } 16 | 17 | return new OrganizerSnippet($organizers->first()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/App/ExternalEvents/AsynchronousExternalEventPublisher.php: -------------------------------------------------------------------------------- 1 | producer->produce($eventType, $eventData); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/MeetupOrganizing/Entity/MeetupId.php: -------------------------------------------------------------------------------- 1 | uuid(); 14 | } 15 | 16 | public static function fromString(string $id): self 17 | { 18 | return new self($id); 19 | } 20 | 21 | public function asString(): string 22 | { 23 | return $this->id; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/MeetupOrganizing/Entity/RsvpId.php: -------------------------------------------------------------------------------- 1 | uuid(); 15 | } 16 | 17 | public static function fromString(string $id): self 18 | { 19 | return new self($id); 20 | } 21 | 22 | public function asString(): string 23 | { 24 | return $this->id; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/AppTest/PageObject/CreateInvoicePage.php: -------------------------------------------------------------------------------- 1 | submitForm('Create invoice', [ 14 | 'year' => $year, 15 | 'month' => $month, 16 | ]); 17 | 18 | self::assertSuccessfulResponse($browser); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/AppTest/PageObject/GenericPage.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public function getFlashMessages(): array 13 | { 14 | $nodes = $this->crawler->filter('.flash-message'); 15 | if (count($nodes) === 0) { 16 | return []; 17 | } 18 | 19 | return array_map(fn (\DOMNode $node) => trim($node->textContent), iterator_to_array($nodes)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/App/ApplicationInterface.php: -------------------------------------------------------------------------------- 1 | asString())); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/AppTest/PageObject/MeetupSnippet.php: -------------------------------------------------------------------------------- 1 | crawler->filter('.name')->text()); 14 | } 15 | 16 | public function readMore(HttpBrowser $browser): MeetupDetailsPage 17 | { 18 | return new MeetupDetailsPage($browser->click($this->crawler->filter('a.read-more')->link())); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/AppTest/PageObject/RescheduleMeetupPage.php: -------------------------------------------------------------------------------- 1 | submitForm('Reschedule', [ 14 | 'scheduleForDate' => $date, 15 | 'scheduleForTime' => $time, 16 | ]); 17 | 18 | self::assertSuccessfulResponse($browser); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /templates/error/error.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@layout/default.html.twig' %} 2 | 3 | {% block title %}{{ status }} {{ reason }}{% endblock %} 4 | 5 | {% block content %} 6 |We encountered a {{ status }} {{ reason }} error.
9 | {% if status == 404 %} 10 |11 | You are looking for something that doesn't exist or may have moved. Check out one of the links on this page 12 | or head back to Home. 13 |
14 | {% endif %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /test/AppTest/ApiTest.php: -------------------------------------------------------------------------------- 1 | browser->request('GET', '/api/ping'); 12 | 13 | $jsonData = $this->browser->getInternalResponse() 14 | ->getContent(); 15 | self::assertJson($jsonData); 16 | 17 | $decodedData = json_decode($jsonData, true); 18 | self::assertIsArray($decodedData); 19 | self::assertArrayHasKey('time', $decodedData); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/AppTest/CancelMeetupTest.php: -------------------------------------------------------------------------------- 1 | signUp('Organizer', 'organizer@gmail.com', 'Organizer'); 12 | $this->login('organizer@gmail.com'); 13 | 14 | $this->scheduleMeetup('Coding Dojo', 'Some description', '2026-10-10', '20:00'); 15 | 16 | $this->cancelMeetup('Coding Dojo'); 17 | 18 | $this->assertUpcomingMeetupDoesNotExist('Coding Dojo'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/MeetupOrganizing/Application/RsvpForMeetup.php: -------------------------------------------------------------------------------- 1 | meetupId; 20 | } 21 | 22 | public function userId(): UserId 23 | { 24 | return UserId::fromString($this->userId); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/App/Json.php: -------------------------------------------------------------------------------- 1 | submitForm('Sign up', [ 14 | 'name' => $name, 15 | 'emailAddress' => $emailAddress, 16 | 'userType' => $userType, 17 | ]); 18 | 19 | self::assertSuccessfulResponse($browser); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/App/Entity/EventRecordingCapabilities.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | private array $events = []; 13 | 14 | /** 15 | * @return array