├── .env ├── .env.test ├── .gitattributes ├── .gitignore ├── Makefile ├── README.md ├── bin ├── .env ├── console ├── phpunit └── test │ ├── .env │ ├── clean.sh │ ├── init.sh │ ├── mysql │ └── deploy.sh │ └── rabbitmq │ └── deploy.sh ├── composer.json ├── composer.lock ├── config ├── bootstrap.php ├── bundles.php ├── modules │ ├── Email │ │ ├── doctrine.php │ │ └── services.yaml │ ├── TodoList │ │ ├── doctrine.php │ │ └── services.yaml │ └── User │ │ ├── doctrine.php │ │ └── services.yaml ├── packages │ ├── cache.yaml │ ├── dev │ │ └── monolog.yaml │ ├── doctrine.php │ ├── framework.yaml │ ├── mailer.yaml │ ├── messenger.yaml │ ├── nelmio_api_doc.yaml │ ├── prod │ │ ├── doctrine.yaml │ │ ├── monolog.yaml │ │ └── routing.yaml │ ├── routing.yaml │ ├── sensio_framework_extra.yaml │ ├── test │ │ ├── framework.yaml │ │ ├── monolog.yaml │ │ └── twig.yaml │ └── twig.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ ├── dev │ │ └── framework.yaml │ └── nelmio_api_doc.yaml └── services.yaml ├── phpunit.xml.dist ├── public └── index.php ├── src ├── Core │ ├── Account │ │ ├── AccountContextChangeEvent.php │ │ ├── AccountContextController.php │ │ ├── AccountContextControllerEventSubscriber.php │ │ ├── AccountContextService.php │ │ └── Doctrine │ │ │ ├── AccountConnectionParamsProvider.php │ │ │ └── DoctrineAccountContextService.php │ ├── Command │ │ └── Account │ │ │ ├── AccountCommand.php │ │ │ ├── CreateDoctrineDatabaseAccountCommand.php │ │ │ └── DropDoctrineDatabaseAccountCommand.php │ ├── Doctrine │ │ ├── DoctrineConnectionFactory.php │ │ ├── DoctrineDomainRepositoryImplementCompilerPass.php │ │ ├── DoctrinePersistenceException.php │ │ ├── DomainDoctrineRepositoryBase.php │ │ ├── DynamicConnection.php │ │ ├── ModularDoctrineConfigLoader.php │ │ └── Type │ │ │ ├── DoctrineType.php │ │ │ ├── EnumDoctrineType.php │ │ │ └── IntIdDoctrineType.php │ ├── Domain │ │ ├── Exception │ │ │ ├── DomainException.php │ │ │ ├── NotFoundException.php │ │ │ └── PersistenceException.php │ │ └── IntValue.php │ ├── Maker │ │ └── MakeModule.php │ ├── Message │ │ ├── Command │ │ │ ├── Command.php │ │ │ ├── CommandBus.php │ │ │ ├── CommandHandler.php │ │ │ └── CommandHandlerService.php │ │ ├── Event │ │ │ ├── Event.php │ │ │ ├── EventBus.php │ │ │ ├── EventHandler.php │ │ │ └── EventHandlerService.php │ │ ├── Message.php │ │ ├── MessageBusBase.php │ │ ├── MessageHandlerService.php │ │ └── Query │ │ │ ├── Query.php │ │ │ ├── QueryBus.php │ │ │ ├── QueryHandler.php │ │ │ └── QueryHandlerService.php │ ├── Rest │ │ ├── Controller │ │ │ ├── CommandController.php │ │ │ ├── CommandQueryController.php │ │ │ ├── Controller.php │ │ │ └── QueryController.php │ │ └── EventSubscriber │ │ │ ├── JsonBodyRequestSubscriber.php │ │ │ └── KernelExceptionSubscriber.php │ ├── Template │ │ └── TemplateService.php │ └── Util │ │ └── SimpleJsonable.php ├── Kernel.php └── Module │ ├── Email │ ├── Application │ │ └── Command │ │ │ └── SendTemplatedEmail │ │ │ ├── SendTemplatedEmailCommand.php │ │ │ └── SendTemplatedEmailCommandHandler.php │ ├── Domain │ │ └── SharedKernel │ │ │ └── ValueObject │ │ │ ├── Email.php │ │ │ └── TemplatedEmailContent.php │ └── Infrastructure │ │ └── Persistence │ │ └── Doctrine │ │ └── EmailDoctrineType.php │ ├── TodoList │ ├── Application │ │ ├── Command │ │ │ └── Task │ │ │ │ ├── AssignUserCommand.php │ │ │ │ ├── BasicCommandHandlerService.php │ │ │ │ └── CreateCommand.php │ │ ├── Event │ │ │ └── Task │ │ │ │ ├── TaskCreatedEventHandler.php │ │ │ │ └── UserAssignedEventHandler.php │ │ └── Query │ │ │ └── Task │ │ │ ├── BasicQueryHandlerService.php │ │ │ ├── FindByIdQuery.php │ │ │ └── GetListQuery.php │ ├── Domain │ │ ├── Entity │ │ │ └── Task.php │ │ ├── Event │ │ │ ├── TaskCreatedEvent.php │ │ │ └── UserAssignedEvent.php │ │ ├── Repository │ │ │ └── TaskRepository.php │ │ ├── SharedKernel │ │ │ └── ValueObject │ │ │ │ └── TaskId.php │ │ └── ValueObject │ │ │ └── TaskStatus.php │ ├── Infrastructure │ │ ├── Persistence │ │ │ └── Doctrine │ │ │ │ ├── Task.orm.xml │ │ │ │ ├── TaskDoctrineRepository.php │ │ │ │ └── TaskIdDoctrineType.php │ │ └── Rest │ │ │ └── Controller │ │ │ └── TaskController.php │ └── README.md │ └── User │ ├── Application │ ├── Command │ │ └── User │ │ │ ├── BasicUserCommandHandlerService.php │ │ │ ├── CreateCommand.php │ │ │ └── SendNotification │ │ │ ├── SendEmailNotificationCommand.php │ │ │ └── SendEmailNotificationCommandHandler.php │ └── Query │ │ └── User │ │ ├── BasicQueryHandlerService.php │ │ ├── ExistsQuery.php │ │ ├── GetByIdQuery.php │ │ └── GetListQuery.php │ ├── Domain │ ├── Entity │ │ └── User.php │ ├── Repository │ │ └── UserRepository.php │ └── SharedKernel │ │ └── ValueObject │ │ └── UserId.php │ └── Infrastructure │ ├── Persistence │ └── Doctrine │ │ ├── User.orm.xml │ │ ├── UserDoctrineRepository.php │ │ └── UserIdDoctrineType.php │ └── Rest │ └── Controller │ └── UserController.php ├── symfony.lock ├── template_env.local ├── templates ├── base.html.twig └── modules │ └── TodoList │ └── AssignedUserToTaskEmailNotification.twig └── tests ├── bootstrap.php ├── functional └── App │ └── .gitkeep ├── integration └── App │ └── .gitkeep └── unit └── App └── Core └── Message ├── Command └── CommandBusTest.php ├── Event └── EventBusTest.php └── Query └── QueryBusTest.php /.env: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | # the latter taking precedence over the former: 3 | # 4 | # * .env contains default values for the environment variables needed by the app 5 | # * .env.local uncommitted file with local overrides 6 | # * .env.$APP_ENV committed environment-specific defaults 7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides 8 | # 9 | # Real environment variables win over .env files. 10 | # 11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. 12 | # 13 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). 14 | # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration 15 | 16 | ###> symfony/framework-bundle ### 17 | APP_ENV=dev 18 | APP_SECRET=5e909340e130c9cad744e1f63f1cf3c1 19 | #TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 20 | #TRUSTED_HOSTS='^(localhost|example\.com)$' 21 | ###< symfony/framework-bundle ### 22 | 23 | ###> symfony/messenger ### 24 | # Choose one of the transports below 25 | # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages 26 | # MESSENGER_TRANSPORT_DSN=doctrine://default 27 | # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages 28 | MESSENGER_TRANSPORT_DSN=sync:// 29 | ###< symfony/messenger ### 30 | 31 | ###> doctrine/doctrine-bundle ### 32 | # Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url 33 | # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" 34 | # For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8" 35 | # IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml 36 | DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7 37 | ###< doctrine/doctrine-bundle ### 38 | 39 | ###> symfony/mailer ### 40 | # MAILER_DSN=smtp://localhost 41 | ###< symfony/mailer ### 42 | 43 | ###> symfony/google-mailer ### 44 | # Gmail SHOULD NOT be used on production, use it in development only. 45 | # MAILER_DSN=gmail://USERNAME:PASSWORD@default 46 | ###< symfony/google-mailer ### 47 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | /.env.local 3 | /.env.local.php 4 | /.env.*.local 5 | /config/secrets/prod/prod.decrypt.private.php 6 | /public/bundles/ 7 | /var/ 8 | /vendor/ 9 | ###< symfony/framework-bundle ### 10 | 11 | /.idea 12 | bin/dockerutil 13 | bin/MakeHelp 14 | 15 | ###> symfony/phpunit-bridge ### 16 | .phpunit 17 | .phpunit.result.cache 18 | /phpunit.xml 19 | ###< symfony/phpunit-bridge ### 20 | 21 | ###> phpunit/phpunit ### 22 | /phpunit.xml 23 | .phpunit.result.cache 24 | ###< phpunit/phpunit ### 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export BIN_SCRIPTS_ROOT_PATH = ./bin 2 | export DOCKERUTIL_PATH = $(BIN_SCRIPTS_ROOT_PATH)/dockerutil 3 | export MAKEHELP_PATH = $(BIN_SCRIPTS_ROOT_PATH)/MakeHelp 4 | export PROJECT = test_symfony 5 | export SUBPROJECT ?= api 6 | export SUBPROJECTS = api 7 | export ENVIRONMENT ?= test 8 | export BIN_SCRIPTS_PATH = bin/$(ENVIRONMENT) 9 | export IMAGE_DOCKERFILE = Dockerfile 10 | export IMAGE_BUILD_TARGET = production_$(SUBPROJECT) 11 | export IMAGE_SUBPROJECT = $(SUBPROJECT) 12 | export IMAGE_BUILD_CONTEXT = . 13 | 14 | export ACCOUNT ?= 0 15 | 16 | ifeq "$(ENVIRONMENT)" "test" 17 | LOCAL_REGISTRY_PORT = 5000 18 | export IMAGE_NAMESPACE = localhost:$(LOCAL_REGISTRY_PORT) 19 | export VERSION = test 20 | endif 21 | 22 | ifeq "$(ENVIRONMENT)" "prod" 23 | export IMAGE_NAMESPACE = "" 24 | endif 25 | 26 | export IMAGE = $(IMAGE_NAMESPACE)/$(PROJECT)_$(IMAGE_SUBPROJECT):$(VERSION) 27 | 28 | INFO = Showing targets for all of ENVIRONMENT(default: test) and SUBPROJECT(default: api) 29 | EXTRA_MAKE_ARGS = ENVIRONMENT=test|prod SUBPROJECT=$(SUBPROJECTS) 30 | HELP_TARGET_MAX_CHAR_NUM = 30 31 | HAS_DEPS = yes 32 | .DEFAULT_GOAL := help 33 | 34 | ifeq ("$(wildcard $(MAKEHELP_PATH))","") 35 | $(info MakeHelp does not exists execute: 'make deps') 36 | HAS_DEPS = 37 | endif 38 | ifeq ("$(wildcard $(DOCKERUTIL_PATH))","") 39 | $(info dockerutil does not exists execute: 'make deps') 40 | HAS_DEPS = 41 | endif 42 | 43 | ## Download external necessary libs 44 | deps: 45 | @echo "installing.." 46 | @curl -sS https://raw.githubusercontent.com/SAREhub/php_dockerutil/0.3.10/bin/dockerutil > ${DOCKERUTIL_PATH} 47 | @curl -sS https://raw.githubusercontent.com/SAREhub/php_dockerutil/0.3.10/bin/MakeHelp > ${MAKEHELP_PATH} 48 | @echo "installed" 49 | 50 | ifdef HAS_DEPS 51 | include $(MAKEHELP_PATH) 52 | 53 | ifneq ($(MAKECMDGOALS),) 54 | ifneq ($(MAKECMDGOALS),help) 55 | ifndef VERSION 56 | $(error VERSION is not set) 57 | endif 58 | endif 59 | endif 60 | 61 | # SYMFONY 62 | ## Clears Symfony cache 63 | sf_cc: 64 | @php bin/console cache:clear 65 | 66 | ## Runs localc Symfony server 67 | sf_start_local_server: 68 | symfony server:start --no-ansi 69 | # END SYMFONY 70 | 71 | ## Builds and pushes selected subproject image 72 | deploy_image: 73 | @bash bin/deploy_image.sh 74 | 75 | deploy_service: 76 | @bash "${BIN_SCRIPTS_PATH}/$(SUBPROJECT)/deploy.sh" 77 | 78 | ifeq "$(ENVIRONMENT)" "test" 79 | ## Runs docker local registry container for pushing test images 80 | test_init_local_registry: 81 | docker run -d -p $(LOCAL_REGISTRY_PORT):5000 --restart=always --name registry registry:2 82 | 83 | ## Clean test env and inits all depending services like database 84 | test_init: test_clean test_init_base test_init_deps_services 85 | 86 | ## Creates test secrets and network 87 | test_init_base: 88 | @bash ${BIN_SCRIPTS_PATH}/init.sh 89 | 90 | test_init_deps_services: test_init_mysql test_init_rabbitmq 91 | 92 | ## Creates mysql service 93 | test_init_mysql: 94 | @bash $(BIN_SCRIPTS_PATH)/mysql/deploy.sh 95 | 96 | ## Creates rabbitmq service 97 | test_init_rabbitmq: 98 | @bash $(BIN_SCRIPTS_PATH)/rabbitmq/deploy.sh 99 | 100 | ## deploys current image and service 101 | test_update_service: deploy_image deploy_service 102 | 103 | ## Clean test env 104 | test_clean: 105 | bash $(BIN_SCRIPTS_PATH)/clean.sh 106 | endif 107 | 108 | endif 109 | 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Przykładowy projekt modularnego monolitu z wykorzystanie Symfony 5 + DDD, CQRS 2 | Architektura modularnego monolitu(więcej o architekturze: [tutaj](http://www.kamilgrzybek.com/design/modular-monolith-primer/)) w którym moduł będą oparte na architekturze heksagonalnej. Projekt będzie oparty o Symfony 5.0 dostosowanym do modularnego rozbijania aplikacji i wycinania ich w łatwy sposób do oddzielnych microserwisów. Modyfikacja obejmuje też dostosowanie obsługi środowiska multi-tenancy(wielu kont klientów). 3 | 4 | # Core 5 | Zawiera kod współdzielony przez wszystkie moduły ułatwiający budowanie modularnego monolitu, w tym zawiera kod do obsługi zmiany kontekstu konta(proces zmiany kontekstu konta powinien być niewidoczny dla modułów). 6 | 7 | ## Doctrine ORM 8 | ### Konfiguracja 9 | #### Bazy danych: 10 | - **system** - zawiera tabele z modułów systemowych, ma własne połączenie i EntityManagera, który rejestruje tylko elementy ze wszystkich modułów oznaczonych jako systemowe. 11 | - **account_** - zawiera tabele z modułów konta, ma własne połączenie(specjalny mechanizm dynamicznie wybiera bazę na podstawie przekazanego id konta - mechanizm przewiduje wiele hostów baz) i EntityManagera, który rejestruje tylko elementy ze wszystkich modułów oznaczonych jako moduł konta. 12 | 13 | 14 | # Moduł 15 | ## Podział 16 | - **Systemowy** - to moduły, które nie potrzebuje do swojego działania żadnego konta, są to jakieś moduły administracyjne. 17 | - **Konta** - to moduły, które potrzebują do swojego działania kontekstu konta. Ważne, moduł nie powinien wiedzieć, które obecnie konto jest wybrane. 18 | ## Struktura katalogów 19 | - **Domain** - zawiera logikę biznesową(jest nie powiązana z żadnym frameworkiem) wystawia porty. 20 | - **Application** - zawiera punkty wejścia do domeny(Serwisy Aplikacji, CQRS). 21 | - **Infrastructure** - zawiera adaptery do komunikacji ze światem zew. np. RestApi, obsługę Doctrina, Messaging itp. 22 | 23 | ## Domain 24 | Serce każdego modułu zawiera logikę biznesową nie powiązaną z żadnym frameworkiem. W katalogu obowiązuje podział pod względem building bloków DDD: 25 | - **Entity** - zawiera wszystkie encje domeny 26 | - **Repository** - zawiera wszystkie repozytoria domeny 27 | - **ValueObject** - tutaj znajdziemy wszystkie obiekty wartości danej domeny 28 | - **SharedKernel** - zawiera elementy domeny które mogą wykorzystywać inne moduły 29 | - **Service** - serwisy domenowe 30 | - **Exception** - wyjątki ściśle powiązane z daną domeną 31 | - **Policy** - polityki 32 | 33 | ## Application 34 | Symfony w projekcie jest tak skonfigurowane, że z automatu zarejestruje wszystkie handlery komend, zapytań i eventów dla danego modułu, muszą tylko zaimplementować odpowiednie interfejsy markerów z Core. 35 | 36 | - **Command** - zawiera wszystkie komendy do domeny, które można wykonać w danym module, struktura: 37 | - złożone komendy mają podfolder z nazwą komendy i odpowiednią klasę komendy i handlera do niej 38 | - **[nazwa]Command** 39 | - implementuje: App\Core\Message\Command\Command, 40 | - zawiera dane do wykonania komendy, 41 | - immutable - tylko gettery 42 | - **[nazwa]CommandHandler** 43 | - implementuje: **App\Core\Message\Command\CommandHandler** 44 | - **__invoke([nazwa]Command $command): void** 45 | - **BasicCommandHandlerService** 46 | - implementuje: **App\Core\Message\Command\CommandHandlerService** 47 | - powinien zawierać prostesze komendy 48 | - **handle[nazwa bez suffixa]\([nazwa]Command $command): void** 49 | - **Query** - zawiera wszystkie zapytania do domeny, które można zadać w danym module 50 | - złożone zapytania mają podfolder z nazwą zapytania i odpowiednią klasę zapytania i handlera do niego 51 | - **[nazwa]Query** 52 | - implementuje: **App\Core\Message\Query\Query**, 53 | - zawiera dane do wykonania zapytania, 54 | - immutable - tylko gettery 55 | - **[nazwa]QueryHandler** 56 | - implementuje: **App\Core\Message\Query\QueryHandler** 57 | - **__invoke([nazwa]Query $query): void** 58 | - **BasicQueryHandlerService** 59 | - implementuje: **App\Core\Message\Query\QueryHandlerService** 60 | - powinien zawierać prostesze zapytania 61 | - **handle[nazwa bez suffixa]\([nazwa]Query $query): mixed** 62 | - **Event** - zawiera wszystkie handlery zdarzeń dla domeny 63 | - **[nazwa]EventHandler** 64 | - implementuje: **App\Core\Message\Event\EventHandler** 65 | - **__invoke([nazwa]Event $event): void** 66 | - **BasicEventHandlerService** 67 | - implementuje: **App\Core\Message\Event\EventHandlerService** 68 | - powinien zawierać proste handlery zdarzeń 69 | - handle[nazwa bez suffixa]\([nazwa]Event $event): void** 70 | 71 | ## Infrastructure 72 | Zawiera wszystko co potrzebne do kontaktu ze światem zewnętrznym(obsługa bazy danych, rest api). 73 | ### REST 74 | Moduły które udostępniają REST api komunikują się z domeną poprzez komendy i zapytania, kontroler musi: 75 | - znajdować się w **App\Module\\Infrastructure\Rest\Controller** 76 | - dziedziczyć po wybranej bazie kontrolera z **App\Core\Rest\Controller** 77 | 78 | Symfony jest tak skonfigurowane że automatycznie zarejestruje routy kontrolera na podstawie markera: **App\Core\Rest\Controller\Controller** 79 | 80 | ### Doctrine 81 | - wszyskto co związane z Doctrinem wędruje do podkatalogu **Persistence\Doctrine** 82 | - Dla uproszczenia mapowania zastosowano xmla. 83 | - format nazwy pliku: **[nazwa-encji].orm.xml** 84 | - Można implementować własne typy, które będą potem rejestrowane poprzez odpowiedni wpis w configu modułu. 85 | - Nazwy tabel posiadają prefix identyfikatora modułu. 86 | - Repozytoria mogą korzystać z pomocniczych bazowych klas z Core. 87 | ## Config 88 | Część rzeczy, które da się zrobić ogólnie dla wszystkich modułów jak automatycznie rejestrowanie kontrolerów są robione w domyślnym Symfonowym config/services.yaml. 89 | Elementy specyficzne dla danego modułu znajdują się w **config/modules/**: 90 | - **services.yaml** - importowany w głównych serwisach 91 | - **doctrine.php** - zawiera dodatkową konfigurację Doctrina dla modułu 92 | - **isAccountModule** - definiuje czy moduł działa w kontekście konta - wtedy rejestrowane jest mapowania dla EntityManagera od kont 93 | - **enumTypes** - lista klas które należy zarejestrować jako typ enum w Doctrinie 94 | - **customTypes** - lista klas które należy zarejestrować jako typ w Doctrinie 95 | 96 | 97 | ## Przygotowanie środowiska 98 | 99 | #### Narzędzia 100 | 101 | Należy zainstalować niezbędne narzędzia: 102 | 103 | - Docker 104 | - bash (Cygwin dla Windows) 105 | - make 106 | - curl 107 | 108 | #### Wymagane dane środowiskowe 109 | * Testowe konto smtp(może być gmail - symfony ma już zaciągnięte zależności) do wysyłki email 110 | * Skopiować template_env.local pod env.local i wpisać dane 111 | 112 | #### Make 113 | 114 | Przed rozpoczęciem korzystania z make uruchom: 115 | 116 | ```bash 117 | make deps 118 | ``` 119 | 120 | Wszelkie potrzebne operację podczas testów jaki i na produkcji 121 | wykonać poprzez wywołanie make z odpowiednimi parametrami, 122 | żeby zobaczyć wszystkie target wywołaj: 123 | 124 | ```bash 125 | make help 126 | ``` 127 | 128 | Dla targetów można wybrać środowisko uruchomienia: 129 | 130 | ```bash 131 | make ENVIRONMENT=test|prod [SUBPROJECT=] 132 | ``` 133 | 134 | Domyślnym subprojectem w tym serwisie jest **api**. 135 | 136 | #### Budowanie obrazu i wypchanie do rejestru 137 | 138 | Obraz można zbudować dla wybranego środowiska poprzez wywołanie: 139 | 140 | ```bash 141 | make SUBPROJECT= deploy_image 142 | ``` 143 | 144 | #### Uruchomienie lokalne 145 | 146 | Zainicjowanie lokalnego środowiska testowego: 147 | 148 | ```bash 149 | make test_init 150 | ``` 151 | 152 | Usuwanie lokalnego środowiska testowego: 153 | 154 | ```bash 155 | make test_clean 156 | ``` 157 | 158 | Aby uruchomić serwis subprojektu należy wywołać: 159 | 160 | ```bash 161 | make SUBPROJECT= deploy_service 162 | ``` 163 | 164 | ## Uruchamianie i testowanie przykładu 165 | 1. środowisko 166 | ```bash 167 | make test_init 168 | make sf_start_local_server 169 | php bin/console app:account:doctrine:create --account-id 1 170 | ``` 171 | 2. Interaktywne api znajduje się po uruchomieniu pod adresem **http://127.0.0.1:8000/api/doc** 172 | 173 | -------------------------------------------------------------------------------- /bin/.env: -------------------------------------------------------------------------------- 1 | SUBPROJECT_SERVICE="${PROJECT}_${SUBPROJECT}" 2 | SERVICE_WORKERID="{{.Task.Slot}}" 3 | NETWORK=${PROJECT} 4 | 5 | DEFAULT_SERVICE_RESTART_CONDITION="any" 6 | DEFAULT_SERVICE_RESTART_DELAY="10s" 7 | DEFAULT_SERVICE_RESTART_MAX_ATTEMPTS=5 8 | DEFAULT_SERVICE_RESTART_WINDOW="2m" 9 | DEFAULT_SERVICE_RESTART=( \ 10 | --restart-condition=${DEFAULT_SERVICE_RESTART_CONDITION} \ 11 | --restart-delay=${DEFAULT_SERVICE_RESTART_DELAY} \ 12 | --restart-max-attempts=${DEFAULT_SERVICE_RESTART_MAX_ATTEMPTS} \ 13 | --restart-window=${DEFAULT_SERVICE_RESTART_WINDOW} \ 14 | ) 15 | 16 | DEFAULT_SERVICE_UPDATE_DELAY="100ms" 17 | DEFAULT_SERVICE_UPDATE_FAILURE_ACTION="rollback" 18 | DEFAULT_SERVICE_UPDATE_MAX_FAILURE_RATIO="0" 19 | DEFAULT_SERVICE_UPDATE_MONITOR="5s" 20 | DEFAULT_SERVICE_UPDATE_ORDER="stop-first" 21 | DEFAULT_SERVICE_UPDATE_PARALLELISM="0" 22 | DEFAULT_SERVICE_UPDATE=( \ 23 | --update-delay=${DEFAULT_SERVICE_UPDATE_DELAY} \ 24 | --update-failure-action=${DEFAULT_SERVICE_UPDATE_FAILURE_ACTION} \ 25 | --update-max-failure-ratio=${DEFAULT_SERVICE_UPDATE_MAX_FAILURE_RATIO} \ 26 | --update-monitor=${DEFAULT_SERVICE_UPDATE_MONITOR} \ 27 | --update-order=${DEFAULT_SERVICE_UPDATE_ORDER} \ 28 | --update-parallelism=${DEFAULT_SERVICE_UPDATE_PARALLELISM} \ 29 | ) 30 | 31 | DEFAULT_SERVICE_ROLLBACK_DELAY="100ms" 32 | DEFAULT_SERVICE_ROLLBACK_FAILURE_ACTION="continue" 33 | DEFAULT_SERVICE_ROLLBACK_MAX_FAILURE_RATIO="0" 34 | DEFAULT_SERVICE_ROLLBACK_MONITOR="5s" 35 | DEFAULT_SERVICE_ROLLBACK_ORDER="stop-first" 36 | DEFAULT_SERVICE_ROLLBACK_PARALLELISM="0" 37 | DEFAULT_SERVICE_ROLLBACK=( \ 38 | --rollback-delay=${DEFAULT_SERVICE_ROLLBACK_DELAY} \ 39 | --rollback-failure-action=${DEFAULT_SERVICE_ROLLBACK_FAILURE_ACTION} \ 40 | --rollback-max-failure-ratio=${DEFAULT_SERVICE_ROLLBACK_MAX_FAILURE_RATIO} \ 41 | --rollback-monitor=${DEFAULT_SERVICE_ROLLBACK_MONITOR} \ 42 | --rollback-order=${DEFAULT_SERVICE_ROLLBACK_ORDER} \ 43 | --rollback-parallelism=${DEFAULT_SERVICE_ROLLBACK_PARALLELISM} \ 44 | ) 45 | 46 | # ENV: AMQP 47 | AMQP_PREFETCH_COUNT=30 48 | AMQP_PASSWORD_SECRET=${PROJECT}_amqp_password 49 | 50 | # ENV: MYSQL 51 | MYSQL_PASSWORD_SECRET=${PROJECT}_database_password 52 | 53 | # array with secrets names to create in init 54 | SECRETS=($AMQP_PASSWORD_SECRET $MYSQL_PASSWORD_SECRET) 55 | 56 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 23 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 24 | } 25 | 26 | if ($input->hasParameterOption('--no-debug', true)) { 27 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 28 | } 29 | 30 | require dirname(__DIR__).'/config/bootstrap.php'; 31 | 32 | if ($_SERVER['APP_DEBUG']) { 33 | umask(0000); 34 | 35 | if (class_exists(Debug::class)) { 36 | Debug::enable(); 37 | } 38 | } 39 | 40 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 41 | $application = new Application($kernel); 42 | $application->run($input); 43 | -------------------------------------------------------------------------------- /bin/phpunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | /dev/null 22 | } 23 | 24 | set +e 25 | create_test_network 26 | if [ $? -ne 0 ]; then 27 | set -e 28 | sleep 5 29 | create_test_network 30 | fi 31 | dockerutil::print_success "Created network '$NETWORK'" 32 | -------------------------------------------------------------------------------- /bin/test/mysql/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e -u 3 | source $DOCKERUTIL_PATH 4 | set -a 5 | source ./bin/test/.env 6 | set +a 7 | 8 | docker service create \ 9 | --name ${MYSQL_SERVICE} \ 10 | ${COMMON_SERVICE_CREATE_OPTIONS[@]} \ 11 | --publish "${MYSQL_SERVICE_PUBLISH_PORT}:${MYSQL_SERVICE_PORT}" \ 12 | --secret ${MYSQL_PASSWORD_SECRET} \ 13 | --env "MYSQL_ROOT_PASSWORD_FILE=/run/secrets/${MYSQL_PASSWORD_SECRET}" \ 14 | ${MYSQL_SERVICE_IMAGE} 15 | 16 | dockerutil::print_success "Created service: ${MYSQL_SERVICE}, access localhost:${MYSQL_SERVICE_PUBLISH_PORT}" 17 | 18 | docker service create \ 19 | --name "${MYSQL_ADMIN_SERVICE}" \ 20 | ${COMMON_SERVICE_CREATE_OPTIONS[@]} \ 21 | --publish "${MYSQL_ADMIN_PUBLISH_PORT}:${MYSQL_ADMIN_PORT}" \ 22 | --env PMA_HOST=$MYSQL_SERVICE \ 23 | ${MYSQL_ADMIN_SERVICE_IMAGE} >/dev/null 24 | 25 | dockerutil::print_success "Created service: ${MYSQL_ADMIN_SERVICE}, access http://localhost:${MYSQL_ADMIN_PUBLISH_PORT} user: ${MYSQL_USER} password: ${TEST_PASSWORD}" 26 | 27 | -------------------------------------------------------------------------------- /bin/test/rabbitmq/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e -u 3 | source $DOCKERUTIL_PATH 4 | set -a 5 | source ./bin/test/.env 6 | set +a 7 | 8 | docker service create \ 9 | ${COMMON_SERVICE_CREATE_OPTIONS[@]} \ 10 | --name $RABBITMQ_SERVICE \ 11 | --publish ${RABBITMQ_SERVICE_AMQP_PUBLISH_PORT}:${AMQP_PORT} \ 12 | --publish ${RABBITMQ_SERVICE_MANAGEMENT_PUBLISH_PORT}:15672 \ 13 | --hostname $RABBITMQ_SERVICE \ 14 | --limit-cpu 0.5 \ 15 | --limit-memory 500M \ 16 | --env RABBITMQ_DEFAULT_VHOST=${AMQP_VHOST} \ 17 | --env RABBITMQ_DEFAULT_USER=${AMQP_USER} \ 18 | --env RABBITMQ_DEFAULT_PASS=${TEST_PASSWORD} \ 19 | --env RABBITMQ_VM_MEMORY_HIGH_WATERMARK='0.6' \ 20 | ${RABBITMQ_SERVICE_IMAGE} 21 | 22 | dockerutil::print_success "created service: $RABBITMQ_SERVICE" 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "license": "proprietary", 4 | "require": { 5 | "php": "^7.2.5", 6 | "ext-ctype": "*", 7 | "ext-iconv": "*", 8 | "acelaya/doctrine-enum-type": "^2.3", 9 | "beberlei/assert": "^3.2", 10 | "myclabs/php-enum": "^1.7", 11 | "nelmio/api-doc-bundle": "^3.6", 12 | "sensio/framework-extra-bundle": "^5.5", 13 | "symfony/asset": "5.0.*", 14 | "symfony/console": "5.0.*", 15 | "symfony/dotenv": "5.0.*", 16 | "symfony/finder": "5.0.*", 17 | "symfony/flex": "^1.3.1", 18 | "symfony/framework-bundle": "5.0.*", 19 | "symfony/google-mailer": "5.0.*", 20 | "symfony/mailer": "5.0.*", 21 | "symfony/messenger": "5.0.*", 22 | "symfony/monolog-bundle": "^3.5", 23 | "symfony/orm-pack": "^1.0", 24 | "symfony/twig-pack": "^1.0", 25 | "symfony/yaml": "5.0.*" 26 | }, 27 | "require-dev": { 28 | "mockery/mockery": "^1.3", 29 | "phpunit/phpunit": "^8.0", 30 | "symfony/maker-bundle": "^1.15", 31 | "symfony/phpunit-bridge": "^5.0", 32 | "symfony/test-pack": "^1.0" 33 | }, 34 | "config": { 35 | "preferred-install": { 36 | "*": "dist" 37 | }, 38 | "sort-packages": true 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "App\\": "src/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "App\\Tests\\": "tests/" 48 | } 49 | }, 50 | "replace": { 51 | "paragonie/random_compat": "2.*", 52 | "symfony/polyfill-ctype": "*", 53 | "symfony/polyfill-iconv": "*", 54 | "symfony/polyfill-php72": "*", 55 | "symfony/polyfill-php71": "*", 56 | "symfony/polyfill-php70": "*", 57 | "symfony/polyfill-php56": "*" 58 | }, 59 | "scripts": { 60 | "auto-scripts": { 61 | "cache:clear": "symfony-cmd", 62 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 63 | }, 64 | "post-install-cmd": [ 65 | "@auto-scripts" 66 | ], 67 | "post-update-cmd": [ 68 | "@auto-scripts" 69 | ] 70 | }, 71 | "conflict": { 72 | "symfony/symfony": "*" 73 | }, 74 | "extra": { 75 | "symfony": { 76 | "allow-contrib": false, 77 | "require": "5.0.*" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | =1.2) 9 | if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) { 10 | foreach ($env as $k => $v) { 11 | $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v); 12 | } 13 | } elseif (!class_exists(Dotenv::class)) { 14 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 15 | } else { 16 | // load all the .env files 17 | (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env'); 18 | } 19 | 20 | $_SERVER += $_ENV; 21 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 22 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 23 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 24 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 6 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 7 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 8 | Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true], 9 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 10 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 11 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 12 | ]; 13 | -------------------------------------------------------------------------------- /config/modules/Email/doctrine.php: -------------------------------------------------------------------------------- 1 | true, 7 | "customTypes" => [ 8 | EmailDoctrineType::NAME => EmailDoctrineType::class 9 | ], 10 | ]; 11 | 12 | -------------------------------------------------------------------------------- /config/modules/Email/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | -------------------------------------------------------------------------------- /config/modules/TodoList/doctrine.php: -------------------------------------------------------------------------------- 1 | true, 8 | "enumTypes" => [ 9 | TaskStatus::class 10 | ], 11 | "customTypes" => [ 12 | TaskIdDoctrineType::class 13 | ], 14 | ]; 15 | 16 | -------------------------------------------------------------------------------- /config/modules/TodoList/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | App\Module\TodoList\Domain\Repository\TaskRepository: 3 | tags: ['app.core.doctrine.domain_repository_implementation'] 4 | -------------------------------------------------------------------------------- /config/modules/User/doctrine.php: -------------------------------------------------------------------------------- 1 | true, 8 | "customTypes" => [ 9 | UserIdDoctrineType::class 10 | ], 11 | ]; 12 | 13 | -------------------------------------------------------------------------------- /config/modules/User/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | App\Module\User\Domain\Repository\UserRepository: 3 | tags: ['app.core.doctrine.domain_repository_implementation'] 4 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | stdout: 4 | type: stream 5 | level: debug 6 | process_psr_3_messages: true 7 | channels: ["!event", "!doctrine", "!console"] 8 | formatter: 'monolog.formatter.json' 9 | path: 'php://stdout' 10 | -------------------------------------------------------------------------------- /config/packages/doctrine.php: -------------------------------------------------------------------------------- 1 | load(); 7 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | #csrf_protection: true 4 | #http_method_override: true 5 | 6 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 7 | # Remove or comment this section to explicitly disable session support. 8 | session: 9 | handler_id: null 10 | cookie_secure: auto 11 | cookie_samesite: lax 12 | 13 | #esi: true 14 | #fragments: true 15 | php_errors: 16 | log: true 17 | -------------------------------------------------------------------------------- /config/packages/mailer.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | mailer: 3 | dsn: '%env(MAILER_DSN)%' 4 | -------------------------------------------------------------------------------- /config/packages/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | default_bus: messenger.bus.command 4 | 5 | buses: 6 | messenger.bus.command: 7 | default_middleware: false 8 | middleware: 9 | - handle_message 10 | 11 | messenger.bus.event.async: ~ 12 | 13 | messenger.bus.query: 14 | default_middleware: false 15 | middleware: 16 | - handle_message 17 | 18 | transports: 19 | events: "%env(MESSENGER_TRANSPORT_DSN)%" 20 | 21 | routing: 22 | 'App\Core\Message\Event\Event': events 23 | -------------------------------------------------------------------------------- /config/packages/nelmio_api_doc.yaml: -------------------------------------------------------------------------------- 1 | nelmio_api_doc: 2 | documentation: 3 | host: 127.0.0.1:8000 # todo use env 4 | schemes: [http, https] 5 | info: 6 | title: Simple Symfony Modular Monolith 7 | description: Simple Symfony Modular Monolith 8 | version: 1.0.0 9 | areas: 10 | default: 11 | path_patterns: [ ^/api(?!/doc) ] 12 | account: 13 | path_patterns: [ ^/api/account ] 14 | admin: 15 | path_patterns: [ ^/api/admin ] 16 | -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | port: "123123" 4 | orm: 5 | auto_generate_proxy_classes: false 6 | metadata_cache_driver: 7 | type: pool 8 | pool: doctrine.system_cache_pool 9 | query_cache_driver: 10 | type: pool 11 | pool: doctrine.system_cache_pool 12 | result_cache_driver: 13 | type: pool 14 | pool: doctrine.result_cache_pool 15 | 16 | framework: 17 | cache: 18 | pools: 19 | doctrine.result_cache_pool: 20 | adapter: cache.app 21 | doctrine.system_cache_pool: 22 | adapter: cache.system 23 | -------------------------------------------------------------------------------- /config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: stdout 7 | excluded_http_codes: [404, 405] 8 | buffer_size: 50 9 | stdout: 10 | type: stream 11 | process_psr_3_messages: true 12 | channels: ["!event", "!doctrine", "!console"] 13 | formatter: 'monolog.formatter.json' 14 | path: 'php://stdout' 15 | -------------------------------------------------------------------------------- /config/packages/prod/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: null 4 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: stdout 7 | excluded_http_codes: [404, 405] 8 | channels: ["!event"] 9 | stdout: 10 | type: stream 11 | process_psr_3_messages: true 12 | channels: ["!event", "!doctrine", "!console"] 13 | formatter: 'monolog.formatter.json' 14 | path: 'php://stdout' 15 | level: debug 16 | -------------------------------------------------------------------------------- /config/packages/test/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | strict_variables: true 3 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | #index: 2 | # path: / 3 | # controller: App\Controller\DefaultController::index 4 | -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | api-accounts: 2 | resource: '../../src/Module/*/Infrastructure/Rest/Controller/*' 3 | name_prefix: api_accounts_ 4 | prefix: /api/accounts/{accountId} 5 | requirements: 6 | accountId: '\d+' 7 | type: annotation 8 | 9 | kernel: 10 | resource: ../../src/Kernel.php 11 | type: annotation 12 | -------------------------------------------------------------------------------- /config/routes/dev/framework.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /config/routes/nelmio_api_doc.yaml: -------------------------------------------------------------------------------- 1 | # Expose your documentation as JSON swagger compliant 2 | app.swagger: 3 | path: /api/doc.json 4 | methods: GET 5 | defaults: { _controller: nelmio_api_doc.controller.swagger } 6 | 7 | app.swagger_ui: 8 | path: /api/doc/{area} 9 | methods: GET 10 | defaults: { _controller: nelmio_api_doc.controller.swagger_ui, area: default } 11 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: ./modules/**/services.yaml } 3 | 4 | services: 5 | _defaults: 6 | autowire: true 7 | autoconfigure: true 8 | bind: 9 | $notificationFromEmail: '@core.notification_from_email' 10 | 11 | _instanceof: 12 | 13 | App\Core\Message\Command\CommandHandler: 14 | public: true 15 | tags: 16 | - { name: messenger.message_handler, bus: messenger.bus.command } 17 | 18 | App\Core\Message\Event\EventHandler: 19 | public: true 20 | tags: 21 | - { name: messenger.message_handler, bus: messenger.bus.event.async } 22 | 23 | App\Core\Message\Query\QueryHandler: 24 | public: true 25 | tags: 26 | - { name: messenger.message_handler, bus: messenger.bus.query } 27 | 28 | App\Core\Rest\Controller\Controller: 29 | public: true 30 | tags: ['controller.service_arguments'] 31 | 32 | 33 | App\Core\: 34 | resource: '../src/Core/*' 35 | 36 | App\Core\Account\Doctrine\DoctrineAccountContextService: 37 | arguments: ['@App\Core\Account\Doctrine\AccountConnectionParamsProvider', '@doctrine.orm.account_entity_manager'] 38 | 39 | doctrine.dbal.connection_factory: 40 | class: App\Core\Doctrine\DoctrineConnectionFactory 41 | arguments: ['%doctrine.dbal.connection_factory.types%', '%kernel.project_dir%/config/modules'] 42 | 43 | App\Core\Message\Command\CommandBus: 44 | arguments: 45 | - '@messenger.bus.command' 46 | 47 | App\Core\Message\Event\EventBus: 48 | arguments: 49 | - '@messenger.bus.event.async' 50 | 51 | App\Core\Message\Query\QueryBus: 52 | arguments: 53 | - '@messenger.bus.query' 54 | 55 | App\Module\: 56 | resource: '../src/Module/*' 57 | exclude: '../src/*/{Doctrine/Entity}' 58 | 59 | core.notification_from_email: 60 | class: App\Module\Email\Domain\SharedKernel\ValueObject\Email 61 | arguments: ['%env(NOTIFICATION_FROM_EMAIL)%'] 62 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ./tests/functional/ 19 | 20 | 21 | ./tests/integration/ 22 | 23 | 24 | ./tests/unit/ 25 | 26 | 27 | 28 | 29 | 30 | src 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handle($request); 26 | $response->send(); 27 | $kernel->terminate($request, $response); 28 | -------------------------------------------------------------------------------- /src/Core/Account/AccountContextChangeEvent.php: -------------------------------------------------------------------------------- 1 | accountId = $accountId; 16 | } 17 | 18 | public function getAccountId(): int 19 | { 20 | return $this->accountId; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Core/Account/AccountContextController.php: -------------------------------------------------------------------------------- 1 | service = $service; 18 | } 19 | 20 | public function onKernelController(ControllerEvent $event) 21 | { 22 | if (!$event->isMasterRequest()) { 23 | return; 24 | } 25 | 26 | $controller = $event->getController(); 27 | if (is_array($controller)) { 28 | $controller = $controller[0]; 29 | } 30 | 31 | if ($controller instanceof AccountContextController) { 32 | $accountId = $event->getRequest()->attributes->get(AccountContextController::ACCOUNT_ID_ROUTE_PARAMETER); 33 | $this->service->switchAccount($accountId); 34 | } 35 | } 36 | 37 | public static function getSubscribedEvents() 38 | { 39 | return [ 40 | KernelEvents::CONTROLLER => 'onKernelController', 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Core/Account/AccountContextService.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 16 | } 17 | 18 | public function switchAccount(int $accountId) 19 | { 20 | $this->dispatcher->dispatch(new AccountContextChangeEvent($accountId)); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Core/Account/Doctrine/AccountConnectionParamsProvider.php: -------------------------------------------------------------------------------- 1 | baseParams = [ 16 | "host" => "localhost", 17 | "port" => "10003", 18 | "user" => "root", 19 | "password" => "test", 20 | 'driver' => 'pdo_mysql', 21 | 'server_version' => '5.7', 22 | 'charset' => 'utf8mb4', 23 | ]; 24 | } 25 | 26 | 27 | public function get(string $accountId): array 28 | { 29 | $params = $this->baseParams; 30 | $params["dbname"] = $this->getDatabaseName($accountId); 31 | return $params; 32 | } 33 | 34 | private function getDatabaseName(string $accountId) { 35 | return $this->dbnamePrefix.$accountId; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Core/Account/Doctrine/DoctrineAccountContextService.php: -------------------------------------------------------------------------------- 1 | connectionParamsProvider = $connectionParamsProvider; 20 | $this->entityManager = $entityManager; 21 | } 22 | 23 | public function onAccountContextChange(AccountContextChangeEvent $event): void 24 | { 25 | $this->switchAccount($event->getAccountId()); 26 | } 27 | 28 | public function switchAccount(int $accountId): void 29 | { 30 | $connection = $this->entityManager->getConnection(); 31 | 32 | if ($connection instanceof DynamicConnection) { 33 | $this->entityManager->flush(); 34 | $this->entityManager->clear(); 35 | $connection->switchConnection($accountId, $this->connectionParamsProvider->get($accountId)); 36 | } else { 37 | throw new \LogicException("To switch connection to selected account, connection object must be instance of " . DynamicConnection::class); 38 | } 39 | } 40 | 41 | public static function getSubscribedEvents() 42 | { 43 | return [ 44 | AccountContextChangeEvent::class => "onAccountContextChange" 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Core/Command/Account/AccountCommand.php: -------------------------------------------------------------------------------- 1 | service = $service; 28 | } 29 | 30 | protected function configure() 31 | { 32 | $this 33 | ->addOption(self::OPT_ACCOUNT_ID, "aid", InputOption::VALUE_REQUIRED, "Choose account context "); 34 | } 35 | 36 | public function setName(string $name) 37 | { 38 | return parent::setName(self::COMMAND_NAME_PREFIX.$name); 39 | } 40 | 41 | protected function execute(InputInterface $input, OutputInterface $output) 42 | { 43 | $accountId = (int)$input->getOption(self::OPT_ACCOUNT_ID); 44 | 45 | if ($accountId <= 0) { 46 | $output->writeln("account-id must be numeric and greater then 0"); 47 | return 255; 48 | } 49 | 50 | $this->service->switchAccount($accountId); 51 | $output->writeln("Connection switched to selected account database"); 52 | 53 | return $this->executeCommand($input, $output); 54 | } 55 | 56 | protected abstract function executeCommand(InputInterface $input, OutputInterface $output); 57 | } 58 | -------------------------------------------------------------------------------- /src/Core/Command/Account/CreateDoctrineDatabaseAccountCommand.php: -------------------------------------------------------------------------------- 1 | setName("doctrine:create") 18 | ->setDescription("Creates account mysql database"); 19 | } 20 | 21 | protected function executeCommand(InputInterface $input, OutputInterface $output) 22 | { 23 | $command = $this->getApplication()->find("doctrine:database:create"); 24 | $createDatabaseArguments = new ArrayInput([ 25 | "command" => "doctrine:database:create", 26 | "--connection" => "account", 27 | ]); 28 | if ($command->run($createDatabaseArguments, $output) !== 0) { 29 | return 255; 30 | } 31 | 32 | $command = $this->getApplication()->find("doctrine:schema:create"); 33 | $createDatabaseArguments = new ArrayInput([ 34 | "command" => "doctrine:schema:create", 35 | "--em" => "account", 36 | ]); 37 | if ($command->run($createDatabaseArguments, $output) !== 0) { 38 | return 255; 39 | } 40 | 41 | return 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Core/Command/Account/DropDoctrineDatabaseAccountCommand.php: -------------------------------------------------------------------------------- 1 | setName("doctrine:drop") 19 | ->setDescription("Drops account mysql database") 20 | ; 21 | } 22 | 23 | protected function executeCommand(InputInterface $input, OutputInterface $output) 24 | { 25 | $command = $this->getApplication()->find("doctrine:database:drop"); 26 | $createDatabaseArguments = new ArrayInput([ 27 | "command" => "doctrine:schema:create", 28 | "--connection" => "account", 29 | "--force" => true, 30 | ]); 31 | $command->run($createDatabaseArguments, $output); 32 | 33 | return 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Core/Doctrine/DoctrineConnectionFactory.php: -------------------------------------------------------------------------------- 1 | registerModulesTypes($modulesConfigPath); 19 | } 20 | 21 | private function registerModulesTypes($modulesConfigPath) 22 | { 23 | foreach (ModularDoctrineConfigLoader::loadModuleConfigs($modulesConfigPath) as $moduleId => $config) { 24 | $this->registerModuleEnumTypes($moduleId, $config); 25 | $this->registerModuleCustomTypes($moduleId, $config); 26 | } 27 | } 28 | 29 | private function registerModuleEnumTypes(string $moduleId, array $config) 30 | { 31 | if (isset($config["enumTypes"])) { 32 | foreach ($config["enumTypes"] as $typeClass) { 33 | $typeName = self::createTypeName($moduleId, substr($typeClass, strrpos($typeClass, "\\") + 1)); 34 | if (!EnumDoctrineType::hasType($typeName)) { 35 | EnumDoctrineType::registerEnumType($typeName, $typeClass); 36 | } 37 | } 38 | } 39 | } 40 | 41 | private function registerModuleCustomTypes(string $moduleId, array $config) 42 | { 43 | if (isset($config["customTypes"])) { 44 | foreach ($config["customTypes"] as $typeClass) { 45 | $typeName = self::createTypeName($moduleId, substr($typeClass, strrpos($typeClass, "\\") + 1, -self::TYPE_SUFFIX_LENGTH)); 46 | if (!Type::hasType($typeName)) { 47 | Type::addType($typeName, $typeClass); 48 | } 49 | } 50 | } 51 | } 52 | 53 | private static function createTypeName(string $moduleId, string $baseTypeName): string 54 | { 55 | return "$moduleId.$baseTypeName"; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/Core/Doctrine/DoctrineDomainRepositoryImplementCompilerPass.php: -------------------------------------------------------------------------------- 1 | \w+)\\\\Domain\\\\Repository\\\\(?\w+)Repository$/'; 16 | 17 | public function process(ContainerBuilder $container) 18 | { 19 | $taggedServices = $container->findTaggedServiceIds(self::TAG); 20 | foreach ($taggedServices as $id => $tags) { 21 | if (preg_match(self::DOMAIN_REPOSITORY_CLASS_REGEX, $id, $matches)) { 22 | $this->setupImplementationInService($id, $matches["moduleName"], $matches["entityName"], $container); 23 | } else { 24 | throw new LogicException("Tagged service: '$id' with '" . self::TAG . "' tag hasn't valid id format. Format must be App\\Module\\\\Domain\\Repository\\"); 25 | } 26 | 27 | } 28 | } 29 | 30 | private function setupImplementationInService(string $serviceId, string $moduleName, string $domainEntityName, ContainerBuilder $container) 31 | { 32 | $def = $container->findDefinition($serviceId); 33 | $def->setFactory([new Reference('doctrine.orm.account_entity_manager'), 'getRepository']); 34 | $def->setArguments([$this->generateDoctrineEntityName($moduleName, $domainEntityName)]); 35 | } 36 | 37 | private function generateDoctrineEntityName(string $moduleName, string $domainEntityName) 38 | { 39 | return 'App\\Module\\' . $moduleName . '\\Domain\\Entity\\' . $domainEntityName; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Core/Doctrine/DoctrinePersistenceException.php: -------------------------------------------------------------------------------- 1 | getMessage(), 500, $previous); 18 | } 19 | 20 | public static function create(string $message, Throwable $previous = null): self 21 | { 22 | return new self($message, 500, $previous); 23 | } 24 | 25 | private function __construct($message = "", $code = 0, Throwable $previous = null) 26 | { 27 | parent::__construct($message, $code, $previous); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Core/Doctrine/DomainDoctrineRepositoryBase.php: -------------------------------------------------------------------------------- 1 | getEntityManager()->persist($entity); 14 | $this->getEntityManager()->flush(); 15 | } catch (\Exception $e) { 16 | throw DoctrinePersistenceException::createFromDoctrine($e); 17 | } 18 | } 19 | 20 | public function getList(): \Iterator 21 | { 22 | try { 23 | $entities = parent::findAll(); 24 | return new \ArrayIterator($entities); 25 | } catch (\Exception $e) { 26 | throw DoctrinePersistenceException::createFromDoctrine($e); 27 | } 28 | } 29 | 30 | /** 31 | * 32 | * @param $id mixed Id of entity 33 | * @return mixed | null 34 | */ 35 | protected function tryGetById($id) 36 | { 37 | try { 38 | return $doctrineEntity = $this->find($id); 39 | } catch (\Exception $e) { 40 | throw DoctrinePersistenceException::createFromDoctrine($e); 41 | } 42 | } 43 | 44 | protected function deleteEntity($entity) 45 | { 46 | try { 47 | $this->getEntityManager()->remove($entity); 48 | $this->getEntityManager()->flush(); 49 | } catch (\Exception $e) { 50 | throw DoctrinePersistenceException::createFromDoctrine($e); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Core/Doctrine/DynamicConnection.php: -------------------------------------------------------------------------------- 1 | connectionId = ""; 29 | $this->params = $params; 30 | $this->internalParamsChanger = Closure::bind(function (Connection $c, $newParams) { 31 | foreach ($newParams as $param => $value) { 32 | $c->params[$param] = $value; 33 | } 34 | }, $this, $this); 35 | 36 | } 37 | 38 | public function switchConnection(string $connectionId, array $params): void 39 | { 40 | if ($this->connectionId !== $connectionId) { 41 | $this->close(); 42 | ($this->internalParamsChanger)($this, $params); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Core/Doctrine/ModularDoctrineConfigLoader.php: -------------------------------------------------------------------------------- 1 | container = $container; 18 | $this->modulesConfigPath = $modulesConfigPath; 19 | } 20 | 21 | public function load(): void 22 | { 23 | $moduleConfigs = self::loadModuleConfigs($this->modulesConfigPath); 24 | $this->container->loadFromExtension('doctrine', $this->generateDoctrineConfig($moduleConfigs)); 25 | } 26 | 27 | private function generateDoctrineConfig(array $moduleConfigs): array 28 | { 29 | $systemModulesMappings = []; 30 | $accountModulesMappings = []; 31 | foreach ($moduleConfigs as $moduleName => $moduleConfig) { 32 | $mapping = $this->generateModuleMapping($moduleName); 33 | if ($moduleConfig['isAccountModule']) { 34 | $accountModulesMappings[$moduleName] = $mapping; 35 | } else { 36 | $systemModulesMappings[$moduleName] = $mapping; 37 | } 38 | } 39 | 40 | return [ 41 | 'dbal' => [ 42 | 'default_connection' => 'system', 43 | 'connections' => [ 44 | 'system' => [ // @TODO change to env 45 | 'dbname' => 'system', 46 | 'port' => '10003', 47 | 'user' => 'root', 48 | 'password' => 'test', 49 | 'driver' => 'pdo_mysql', 50 | 'server_version' => '5.7', 51 | 'charset' => 'utf8mb4', 52 | 53 | ], 54 | 'account' => [ // dynamic 55 | 'port' => '10003', 56 | 'user' => 'root', 57 | 'password' => 'test', 58 | 'driver' => 'pdo_mysql', 59 | 'server_version' => '5.7', 60 | 'charset' => 'utf8mb4', 61 | 'wrapper_class' => DynamicConnection::class, 62 | ] 63 | ], 64 | ], 65 | 'orm' => [ 66 | 'default_entity_manager' => 'system', 67 | 'entity_managers' => [ 68 | 'system' => [ 69 | 'connection' => 'system', 70 | 'mappings' => $systemModulesMappings, 71 | ], 72 | 'account' => [ 73 | 'connection' => 'account', 74 | 'mappings' => $accountModulesMappings 75 | ], 76 | ], 77 | ], 78 | ]; 79 | } 80 | 81 | public static function loadModuleConfigs(string $modulesConfigPath): array 82 | { 83 | $finder = new Finder(); 84 | $finder->files()->name('doctrine.php'); 85 | 86 | $modules = []; 87 | /** @var SplFileInfo $moduleConfigFile */ 88 | foreach ($finder->in($modulesConfigPath) as $moduleConfigFile) { 89 | $moduleName = basename($moduleConfigFile->getPath()); 90 | $modules[$moduleName] = include($moduleConfigFile->getPathname()); 91 | } 92 | return $modules; 93 | } 94 | 95 | private function generateModuleMapping(string $moduleName): array 96 | { 97 | return [ 98 | 'type' => 'xml', 99 | 'dir' => '%kernel.project_dir%/src/Module/' . $moduleName . '/Infrastructure/Persistence/Doctrine', 100 | 'is_bundle' => false, 101 | 'prefix' => 'App\\Module\\' . $moduleName . '\\Domain\\Entity', 102 | 'alias' => $moduleName, 103 | ]; 104 | } 105 | 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/Core/Doctrine/Type/DoctrineType.php: -------------------------------------------------------------------------------- 1 | enumClass, 'toArray']); 16 | return \sprintf( 17 | 'ENUM("%s") COMMENT "%s"', 18 | \implode('", "', $values), 19 | $this->getName() 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Core/Doctrine/Type/IntIdDoctrineType.php: -------------------------------------------------------------------------------- 1 | getIntegerTypeDeclarationSQL(array_merge([ 16 | "unsigned" => true 17 | ], $fieldDeclaration)); 18 | } 19 | 20 | /** 21 | * @param int $value 22 | * @param AbstractPlatform $platform 23 | * @return IntValue 24 | */ 25 | public function convertToPHPValue($value, AbstractPlatform $platform) 26 | { 27 | return $this->createFromValue($value); 28 | } 29 | 30 | protected abstract function createFromValue(int $value): IntValue; 31 | 32 | /** 33 | * @param IntValue $value 34 | * @param AbstractPlatform $platform 35 | * @return int 36 | */ 37 | public function convertToDatabaseValue($value, AbstractPlatform $platform) 38 | { 39 | return $value->getRaw(); 40 | } 41 | 42 | public function requiresSQLCommentHint(AbstractPlatform $platform) 43 | { 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Core/Domain/Exception/DomainException.php: -------------------------------------------------------------------------------- 1 | value = $value; 14 | } 15 | 16 | public abstract function isEmpty(): bool; 17 | 18 | public function getRaw(): int 19 | { 20 | return $this->value; 21 | } 22 | 23 | public function equals(IntValue $other): bool 24 | { 25 | return $this->getRaw() === $other->getRaw(); 26 | } 27 | 28 | public function jsonSerialize() 29 | { 30 | return $this->getRaw(); 31 | } 32 | 33 | public function __toString() 34 | { 35 | return (string)$this->getRaw(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Core/Maker/MakeModule.php: -------------------------------------------------------------------------------- 1 | setDescription("Creates a new module in project") 34 | ->addArgument("module-name", InputArgument::REQUIRED, "Choose a name for your module ",) 35 | ; 36 | } 37 | 38 | public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator) 39 | { 40 | $moduleName = $input->getArgument("module-name"); 41 | 42 | $moduleRootDir = $generator->getRootDirectory()."/".self::BASE_MODULE_PATH."/".$moduleName; 43 | 44 | if (file_exists($moduleRootDir)) { 45 | $io->error("Module with selected name exists"); 46 | return; 47 | } 48 | foreach (self::MODULE_DIRS as $moduleLayerDir) { 49 | mkdir($moduleRootDir."/".$moduleLayerDir, 0777, true); 50 | } 51 | 52 | $this->writeSuccessMessage($io); 53 | $io->text("Next: Add some code to module"); 54 | } 55 | 56 | public function configureDependencies(DependencyBuilder $dependencies) 57 | { 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Core/Message/Command/Command.php: -------------------------------------------------------------------------------- 1 | getLogger()->info("Handling command", ["command" => $command]); 21 | $this->dispatchInMessenger($command); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Core/Message/Command/CommandHandler.php: -------------------------------------------------------------------------------- 1 | getLogger()->info("Handling event", ["event" => $event]); 19 | $this->dispatchInMessenger($event); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Core/Message/Event/EventHandler.php: -------------------------------------------------------------------------------- 1 | static::class, 13 | "data" => \Closure::bind(function ($message) { 14 | return get_object_vars($message); 15 | }, $this, static::class)($this) 16 | ]; 17 | } 18 | 19 | public function __toString() 20 | { 21 | return static::class; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Core/Message/MessageBusBase.php: -------------------------------------------------------------------------------- 1 | messengerBus = $messengerBus; 23 | $this->logger = new NullLogger(); 24 | } 25 | 26 | /** 27 | * @param object $message 28 | * @return Envelope 29 | * @throws Throwable 30 | */ 31 | protected function dispatchInMessenger(object $message): Envelope 32 | { 33 | try { 34 | return $this->messengerBus->dispatch($message); 35 | } catch (HandlerFailedException $e) { 36 | $this->throwException($e); 37 | } 38 | } 39 | 40 | /** 41 | * @param HandlerFailedException $exception 42 | * @throws Throwable 43 | */ 44 | private function throwException(HandlerFailedException $exception): void 45 | { 46 | while ($exception instanceof HandlerFailedException) { 47 | $exception = $exception->getPrevious(); 48 | } 49 | 50 | throw $exception; 51 | } 52 | 53 | protected function getLogger(): LoggerInterface 54 | { 55 | return $this->logger; 56 | } 57 | 58 | public function setLogger(LoggerInterface $logger) 59 | { 60 | $this->logger = $logger; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Core/Message/MessageHandlerService.php: -------------------------------------------------------------------------------- 1 | [ 17 | "method" => static::getHandleMethodName($typeClass), 18 | ]; 19 | } 20 | } 21 | 22 | protected abstract static function getHandleMethodName(string $typeClass): string; 23 | } 24 | -------------------------------------------------------------------------------- /src/Core/Message/Query/Query.php: -------------------------------------------------------------------------------- 1 | getLogger()->info("Handling query", ["query" => $query]); 21 | $envelope = $this->dispatchInMessenger($query); 22 | /** @var HandledStamp $stamp */ 23 | $stamp = $envelope->last(HandledStamp::class); 24 | return $stamp->getResult(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Core/Message/Query/QueryHandler.php: -------------------------------------------------------------------------------- 1 | commandBus = $commandBus; 17 | } 18 | 19 | protected function executeCommand(Command $command): void 20 | { 21 | $this->commandBus->handle($command); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Core/Rest/Controller/CommandQueryController.php: -------------------------------------------------------------------------------- 1 | commandBus = $commandBus; 19 | } 20 | 21 | protected function executeCommand(Command $command): void 22 | { 23 | $this->commandBus->handle($command); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Core/Rest/Controller/Controller.php: -------------------------------------------------------------------------------- 1 | queryBus = $queryBus; 19 | } 20 | 21 | /** 22 | * @param Query $query 23 | * @return mixed 24 | * @throws \Throwable 25 | */ 26 | protected function executeQuery(Query $query) 27 | { 28 | return $this->queryBus->handle($query); 29 | } 30 | 31 | protected function jsonResponse($data = null, int $status = Response::HTTP_OK, array $headers = []): JsonResponse 32 | { 33 | return JsonResponse::create($data, $status, $headers) 34 | ->setEncodingOptions(JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_PRETTY_PRINT); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Core/Rest/EventSubscriber/JsonBodyRequestSubscriber.php: -------------------------------------------------------------------------------- 1 | getRequest(); 18 | 19 | if (!$this->isJsonRequest($request)) { 20 | return; 21 | } 22 | 23 | $content = $request->getContent(); 24 | 25 | if (empty($content)) { 26 | return; 27 | } 28 | 29 | if (!$this->transformJsonBody($request)) { 30 | $response = Response::create('Unable to parse json request.', Response::HTTP_BAD_REQUEST); 31 | $event->setResponse($response); 32 | } 33 | } 34 | 35 | private function isJsonRequest(Request $request): bool 36 | { 37 | return 'json' === $request->getContentType(); 38 | } 39 | 40 | private function transformJsonBody(Request $request): bool 41 | { 42 | $data = \json_decode((string) $request->getContent(), true); 43 | 44 | if (\JSON_ERROR_NONE !== \json_last_error()) { 45 | return false; 46 | } 47 | 48 | if (null === $data) { 49 | return true; 50 | } 51 | 52 | $request->request->replace($data); 53 | 54 | return true; 55 | } 56 | 57 | public static function getSubscribedEvents() 58 | { 59 | return [ 60 | KernelEvents::REQUEST => 'onKernelRequest', 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Core/Rest/EventSubscriber/KernelExceptionSubscriber.php: -------------------------------------------------------------------------------- 1 | getThrowable(); 21 | 22 | $response = new JsonResponse(); 23 | $response->headers->set('Content-Type', 'application/vnd.api+json'); 24 | $response->setStatusCode($this->getStatusCode($exception)); 25 | $response->setData($this->getErrorMessage($exception, $response)); 26 | $response->setEncodingOptions(JSON_PRETTY_PRINT); 27 | $event->setResponse($response); 28 | } 29 | 30 | private function getStatusCode(Throwable $exception): int 31 | { 32 | return $this->determineStatusCode($exception); 33 | } 34 | 35 | private function getErrorMessage(Throwable $exception, Response $response): array 36 | { 37 | $error = [ 38 | 'errors' => [ 39 | 'title' => \str_replace('\\', '.', \get_class($exception)), 40 | 'detail' => $this->getExceptionMessage($exception), 41 | 'code' => $exception->getCode(), 42 | 'status' => $response->getStatusCode(), 43 | ], 44 | ]; 45 | 46 | if ('dev' === $this->environment) { 47 | $error = \array_merge( 48 | $error, 49 | [ 50 | 'meta' => [ 51 | 'file' => $exception->getFile(), 52 | 'line' => $exception->getLine(), 53 | 'message' => $exception->getMessage(), 54 | 'trace' => $exception->getTrace(), 55 | 'traceString' => $exception->getTraceAsString(), 56 | ], 57 | ] 58 | ); 59 | } 60 | 61 | return $error; 62 | } 63 | 64 | private function getExceptionMessage(Throwable $exception): string 65 | { 66 | return $exception->getMessage(); 67 | } 68 | 69 | private function determineStatusCode(Throwable $exception): int 70 | { 71 | // Default status code is always 500 72 | $statusCode = Response::HTTP_INTERNAL_SERVER_ERROR; 73 | 74 | switch (true) { 75 | case $exception instanceof HttpExceptionInterface: 76 | $statusCode = $exception->getStatusCode(); 77 | break; 78 | case $exception instanceof NotFoundException: 79 | $statusCode = Response::HTTP_NOT_FOUND; 80 | break; 81 | case $exception instanceof \InvalidArgumentException: 82 | $statusCode = Response::HTTP_BAD_REQUEST; 83 | break; 84 | } 85 | 86 | return $statusCode; 87 | } 88 | 89 | public static function getSubscribedEvents(): array 90 | { 91 | return [ 92 | KernelEvents::EXCEPTION => 'onKernelException', 93 | ]; 94 | } 95 | 96 | public function __construct() 97 | { 98 | $this->environment = (string) \getenv('APP_ENV') ?? 'dev'; 99 | } 100 | 101 | private string $environment; 102 | } 103 | -------------------------------------------------------------------------------- /src/Core/Template/TemplateService.php: -------------------------------------------------------------------------------- 1 | twig = $twig; 17 | } 18 | 19 | public function render(string $templateId, array $context): string 20 | { 21 | $template = $this->loadTemplate($templateId); 22 | return $template->render($context); 23 | } 24 | 25 | public function renderBlocks(string $templateId, array $blocksToRender, array $context): array 26 | { 27 | $template = $this->loadTemplate($templateId); 28 | $renderedBlocks = []; 29 | foreach ($blocksToRender as $blockName) { 30 | $renderedBlocks[$blockName] = $template->renderBlock($blockName, $context); 31 | } 32 | 33 | return $renderedBlocks; 34 | } 35 | 36 | private function loadTemplate(string $templateId): TemplateWrapper 37 | { 38 | return $this->twig->load($templateId); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Core/Util/SimpleJsonable.php: -------------------------------------------------------------------------------- 1 | getProjectDir().'/config/bundles.php'; 22 | foreach ($contents as $class => $envs) { 23 | if ($envs[$this->environment] ?? $envs['all'] ?? false) { 24 | yield new $class(); 25 | } 26 | } 27 | } 28 | 29 | public function build(ContainerBuilder $container) 30 | { 31 | $container->addCompilerPass(new DoctrineDomainRepositoryImplementCompilerPass()); 32 | } 33 | 34 | public function getProjectDir(): string 35 | { 36 | return \dirname(__DIR__); 37 | } 38 | 39 | protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void 40 | { 41 | $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php')); 42 | $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug); 43 | $container->setParameter('container.dumper.inline_factories', true); 44 | $confDir = $this->getProjectDir().'/config'; 45 | 46 | $loader->load($confDir.'/{packages}/*'.self::CONFIG_EXTS, 'glob'); 47 | $loader->load($confDir.'/{packages}/'.$this->environment.'/*'.self::CONFIG_EXTS, 'glob'); 48 | $loader->load($confDir.'/{services}'.self::CONFIG_EXTS, 'glob'); 49 | $loader->load($confDir.'/{services}_'.$this->environment.self::CONFIG_EXTS, 'glob'); 50 | } 51 | 52 | protected function configureRoutes(RouteCollectionBuilder $routes): void 53 | { 54 | $confDir = $this->getProjectDir().'/config'; 55 | 56 | $routes->import($confDir.'/{routes}/'.$this->environment.'/*'.self::CONFIG_EXTS, '/', 'glob'); 57 | $routes->import($confDir.'/{routes}/*'.self::CONFIG_EXTS, '/', 'glob'); 58 | $routes->import($confDir.'/{routes}'.self::CONFIG_EXTS, '/', 'glob'); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Module/Email/Application/Command/SendTemplatedEmail/SendTemplatedEmailCommand.php: -------------------------------------------------------------------------------- 1 | from = $from; 21 | $this->to = $to; 22 | $this->content = $content; 23 | } 24 | 25 | public function getFrom(): Email 26 | { 27 | return $this->from; 28 | } 29 | 30 | public function getTo(): Email 31 | { 32 | return $this->to; 33 | } 34 | 35 | public function getContent(): TemplatedEmailContent 36 | { 37 | return $this->content; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Module/Email/Application/Command/SendTemplatedEmail/SendTemplatedEmailCommandHandler.php: -------------------------------------------------------------------------------- 1 | mailer = $mailer; 20 | $this->templateService = $templateService; 21 | } 22 | 23 | public function __invoke(SendTemplatedEmailCommand $command) 24 | { 25 | $email = $this->buildEmail($command); 26 | $this->mailer->send($email); 27 | } 28 | 29 | private function buildEmail(SendTemplatedEmailCommand $command): SymfonyEmail 30 | { 31 | $content = $command->getContent(); 32 | $renderedContent = $this->templateService->renderBlocks( 33 | $content->getTemplateId(), 34 | ["subject", "text", "html"], 35 | $content->getContext() 36 | ); 37 | 38 | return (new SymfonyEmail()) 39 | ->from($command->getFrom()->getRaw()) 40 | ->to($command->getTo()->getRaw()) 41 | ->subject($renderedContent["subject"]) 42 | ->text($renderedContent["text"]) 43 | ->html($renderedContent["html"]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Module/Email/Domain/SharedKernel/ValueObject/Email.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | public static function create(string $value): self 20 | { 21 | return new self($value); 22 | } 23 | 24 | public function getRaw(): string 25 | { 26 | return $this->value; 27 | } 28 | 29 | public function getLocalPart(): string 30 | { 31 | return $this->getParts()[0]; 32 | } 33 | 34 | public function getDomain(): string 35 | { 36 | return $this->getParts()[1]; 37 | } 38 | 39 | public function getParts(): array 40 | { 41 | return explode("@", $this->value, 2); 42 | } 43 | 44 | public function jsonSerialize() 45 | { 46 | return $this->getRaw(); 47 | } 48 | 49 | public function __toString() 50 | { 51 | return $this->value; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Module/Email/Domain/SharedKernel/ValueObject/TemplatedEmailContent.php: -------------------------------------------------------------------------------- 1 | templateId = $templateId; 15 | $this->context = $context; 16 | } 17 | 18 | public static function create(string $templateId, array $context): self 19 | { 20 | return new self($templateId, $context); 21 | } 22 | 23 | public function extendContextWith(array $context): self 24 | { 25 | return self::create($this->templateId, array_merge($this->context, $context)); 26 | } 27 | 28 | public function getTemplateId(): string 29 | { 30 | return $this->templateId; 31 | } 32 | 33 | public function getContext(): array 34 | { 35 | return $this->context; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Module/Email/Infrastructure/Persistence/Doctrine/EmailDoctrineType.php: -------------------------------------------------------------------------------- 1 | getRaw(); 39 | } 40 | 41 | public function requiresSQLCommentHint(AbstractPlatform $platform) 42 | { 43 | return true; 44 | } 45 | 46 | public function getName() 47 | { 48 | return self::NAME; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Module/TodoList/Application/Command/Task/AssignUserCommand.php: -------------------------------------------------------------------------------- 1 | userId = $userId; 19 | $this->taskId = $taskId; 20 | } 21 | 22 | public function getTaskId(): TaskId 23 | { 24 | return $this->taskId; 25 | } 26 | 27 | public function getUserId(): UserId 28 | { 29 | return $this->userId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Module/TodoList/Application/Command/Task/BasicCommandHandlerService.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 28 | $this->eventBus = $eventBus; 29 | $this->queryBus = $queryBus; 30 | } 31 | 32 | public function handleCreate(CreateCommand $command): void 33 | { 34 | $entity = Task::createNew($command->getName()); 35 | $this->repository->saveOrUpdate($entity); 36 | $this->eventBus->handle(new TaskCreatedEvent($entity->getId())); 37 | } 38 | 39 | public function handleAssignUser(AssignUserCommand $command) 40 | { 41 | $task = $this->repository->getById($command->getTaskId()); 42 | 43 | if ($task->getAssignedUserId()->equals($command->getUserId())) { 44 | return; 45 | } 46 | 47 | if (!$this->isUserExists($command->getUserId())) { 48 | throw new NotFoundException("User entity not found"); 49 | } 50 | 51 | $task->setAssignedUserId($command->getUserId()); 52 | $this->repository->saveOrUpdate($task); 53 | $this->eventBus->handle(new UserAssignedEvent($command->getUserId(), $command->getTaskId())); 54 | } 55 | 56 | private function isUserExists(UserId $userId): bool 57 | { 58 | return $this->queryBus->handle(new ExistsQuery($userId)); 59 | } 60 | 61 | protected static function getSupportedTypes(): array 62 | { 63 | return [ 64 | CreateCommand::class, 65 | AssignUserCommand::class 66 | ]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Module/TodoList/Application/Command/Task/CreateCommand.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | } 17 | 18 | public function getName(): string 19 | { 20 | return $this->name; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Module/TodoList/Application/Event/Task/TaskCreatedEventHandler.php: -------------------------------------------------------------------------------- 1 | commandBus = $commandBus; 26 | $this->queryBus = $queryBus; 27 | $this->notificationFromEmail = $notificationFromEmail; 28 | } 29 | 30 | public function __invoke(UserAssignedEvent $event): void 31 | { 32 | /** @var Task $task */ 33 | $task = $this->queryBus->handle(new FindByIdQuery($event->getTaskId())); 34 | 35 | $this->commandBus->handle(new SendEmailNotificationCommand( 36 | $this->notificationFromEmail, 37 | $event->getUserId(), 38 | TemplatedEmailContent::create("modules/TodoList/AssignedUserToTaskEmailNotification.twig", [ 39 | "task" => [ 40 | "id" => $task->getId()->getRaw(), 41 | "name" => $task->getName() 42 | ] 43 | ]) 44 | )); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Module/TodoList/Application/Query/Task/BasicQueryHandlerService.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 18 | } 19 | 20 | public function handleGetList(GetListQuery $query): \Iterator 21 | { 22 | return $this->repository->getList(); 23 | } 24 | 25 | public function handleFindById(FindByIdQuery $query): Task 26 | { 27 | return $this->repository->getById($query->getId()); 28 | } 29 | 30 | protected static function getSupportedTypes(): array 31 | { 32 | return [ 33 | GetListQuery::class, 34 | FindByIdQuery::class 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Module/TodoList/Application/Query/Task/FindByIdQuery.php: -------------------------------------------------------------------------------- 1 | id = $id; 19 | } 20 | 21 | public function getId(): TaskId 22 | { 23 | return $this->id; 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/Module/TodoList/Application/Query/Task/GetListQuery.php: -------------------------------------------------------------------------------- 1 | id = $id; 23 | $this->name = $name; 24 | $this->createdAt = $createdAt; 25 | $this->status = $status; 26 | $this->assignedUserId = $assignedUserId; 27 | } 28 | 29 | public static function createNew(string $name) 30 | { 31 | return new self( 32 | TaskId::emptyValue(), 33 | $name, 34 | \DateTimeImmutable::createFromFormat("U", time()), 35 | TaskStatus::TODO(), 36 | UserId::emptyValue() 37 | ); 38 | } 39 | 40 | public function getId(): TaskId 41 | { 42 | return $this->id; 43 | } 44 | 45 | public function getName(): string 46 | { 47 | return $this->name; 48 | } 49 | 50 | public function setName(string $name): void 51 | { 52 | $this->name = $name; 53 | } 54 | 55 | public function getCreatedAt(): DateTimeInterface 56 | { 57 | return $this->createdAt; 58 | } 59 | 60 | public function getStatus(): TaskStatus 61 | { 62 | return $this->status; 63 | } 64 | 65 | public function setStatus(TaskStatus $status): void 66 | { 67 | $this->status = $status; 68 | } 69 | 70 | public function getAssignedUserId(): UserId 71 | { 72 | return $this->assignedUserId; 73 | } 74 | 75 | public function setAssignedUserId(UserId $assignedUserId): void 76 | { 77 | $this->assignedUserId = $assignedUserId; 78 | } 79 | 80 | public function jsonSerialize() 81 | { 82 | $data = get_object_vars($this); 83 | $data["createdAt"] = $this->createdAt->format(DATE_ATOM); 84 | return $data; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Module/TodoList/Domain/Event/TaskCreatedEvent.php: -------------------------------------------------------------------------------- 1 | taskId = $taskId; 17 | } 18 | 19 | public function getTaskId(): TaskId 20 | { 21 | return $this->taskId; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Module/TodoList/Domain/Event/UserAssignedEvent.php: -------------------------------------------------------------------------------- 1 | userId = $userId; 19 | $this->taskId = $taskId; 20 | } 21 | 22 | public function getUserId(): UserId 23 | { 24 | return $this->userId; 25 | } 26 | 27 | public function getTaskId(): TaskId 28 | { 29 | return $this->taskId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Module/TodoList/Domain/Repository/TaskRepository.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Module/TodoList/Infrastructure/Persistence/Doctrine/TaskDoctrineRepository.php: -------------------------------------------------------------------------------- 1 | save($task); 19 | } 20 | 21 | public function getById(TaskId $id): Task 22 | { 23 | $entity = $this->tryGetById($id); 24 | if (!$entity) { 25 | throw NotFoundException::create(); 26 | } 27 | 28 | return $entity; 29 | } 30 | 31 | public function delete(Task $task): void 32 | { 33 | $this->deleteEntity($task); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Module/TodoList/Infrastructure/Persistence/Doctrine/TaskIdDoctrineType.php: -------------------------------------------------------------------------------- 1 | executeCommand(new CreateCommand($request->request->get("name"))); 49 | return Response::create("", Response::HTTP_CREATED); 50 | } 51 | 52 | /** 53 | * @Route("/{id}", name="get_by_id", methods={"GET"}) 54 | * 55 | * @SWG\Response( 56 | * response=200, 57 | * description="Returns selected entity by id", 58 | * ) 59 | * @SWG\Response( 60 | * response=404, 61 | * description="When selected entity doesn't exists", 62 | * ) 63 | * @param int $id 64 | * @return Response 65 | * @throws Throwable 66 | */ 67 | public function getById(int $id): Response 68 | { 69 | $entity = $this->executeQuery(new FindByIdQuery(TaskId::create($id))); 70 | 71 | 72 | return $this->jsonResponse($entity); 73 | } 74 | 75 | /** 76 | * @Route("/{id}:assignUser", name="assign_user", methods={"POST"}) 77 | * @SWG\Parameter( 78 | * name="body", 79 | * in="body", 80 | * format="application/json", 81 | * required=true, 82 | * @SWG\Schema( 83 | * type="object", 84 | * @SWG\Property(property="userId", type="integer", example=1) 85 | * ) 86 | * ) 87 | * @SWG\Response( 88 | * response=200, 89 | * description="AssignedUserToTask", 90 | * ) 91 | * @param Request $request 92 | * @return Response 93 | */ 94 | public function assignUser(Request $request): Response 95 | { 96 | $this->executeCommand(new AssignUserCommand( 97 | TaskId::create($request->attributes->get("id")), 98 | UserId::create($request->request->get("userId"))) 99 | ); 100 | return Response::create("", Response::HTTP_OK); 101 | } 102 | 103 | /** 104 | * @Route("", name="list", methods={"GET"}) 105 | * @SWG\Response( 106 | * response=200, 107 | * description="Returns entity list", 108 | * ) 109 | * @return Response 110 | * @throws Throwable 111 | */ 112 | public function list(): Response 113 | { 114 | $result = $this->executeQuery(new GetListQuery()); 115 | $entities = iterator_to_array($result); 116 | return $this->jsonResponse($entities); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Module/TodoList/README.md: -------------------------------------------------------------------------------- 1 | # Simple TodoList module 2 | 3 | Każdy moduł będzie miał własne readme może z linkami do zewnętrznej dokumentacji 4 | -------------------------------------------------------------------------------- /src/Module/User/Application/Command/User/BasicUserCommandHandlerService.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 19 | } 20 | 21 | public function handleCreate(CreateCommand $command): void 22 | { 23 | $entity = User::createNew($command->getName(), Email::create($command->getEmail())); 24 | $this->repository->saveOrUpdate($entity); 25 | } 26 | 27 | protected static function getSupportedTypes(): array 28 | { 29 | return [ 30 | CreateCommand::class 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Module/User/Application/Command/User/CreateCommand.php: -------------------------------------------------------------------------------- 1 | name = $name; 17 | $this->email = $email; 18 | } 19 | 20 | public function getName(): string 21 | { 22 | return $this->name; 23 | } 24 | 25 | public function getEmail(): string 26 | { 27 | return $this->email; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Module/User/Application/Command/User/SendNotification/SendEmailNotificationCommand.php: -------------------------------------------------------------------------------- 1 | from = $from; 21 | $this->to = $to; 22 | $this->content = $content; 23 | } 24 | 25 | public function getFrom(): Email 26 | { 27 | return $this->from; 28 | } 29 | 30 | public function getTo(): UserId 31 | { 32 | return $this->to; 33 | } 34 | 35 | public function getContent(): TemplatedEmailContent 36 | { 37 | return $this->content; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Module/User/Application/Command/User/SendNotification/SendEmailNotificationCommandHandler.php: -------------------------------------------------------------------------------- 1 | queryBus = $queryBus; 22 | $this->commandBus = $commandBus; 23 | } 24 | 25 | public function __invoke(SendEmailNotificationCommand $command) 26 | { 27 | /** @var User $user */ 28 | $user = $this->queryBus->handle(new GetByIdQuery($command->getTo())); 29 | $this->commandBus->handle(new SendTemplatedEmailCommand( 30 | $command->getFrom(), 31 | $user->getEmail(), 32 | $command->getContent()->extendContextWith([ 33 | "user" => [ 34 | "name" => $user->getName() 35 | ] 36 | ]) 37 | )); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Module/User/Application/Query/User/BasicQueryHandlerService.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 18 | } 19 | 20 | public function handleGetList(GetListQuery $query): \Iterator 21 | { 22 | return $this->repository->getList(); 23 | } 24 | 25 | public function handleGetById(GetByIdQuery $query): User 26 | { 27 | return $this->repository->getById($query->getId()); 28 | } 29 | 30 | public function handleExists(ExistsQuery $query): bool 31 | { 32 | return $this->repository->exists($query->getId()); 33 | } 34 | 35 | protected static function getSupportedTypes(): array 36 | { 37 | return [ 38 | GetListQuery::class, 39 | GetByIdQuery::class, 40 | ExistsQuery::class 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Module/User/Application/Query/User/ExistsQuery.php: -------------------------------------------------------------------------------- 1 | id = $id; 17 | } 18 | 19 | public function getId(): UserId 20 | { 21 | return $this->id; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Module/User/Application/Query/User/GetByIdQuery.php: -------------------------------------------------------------------------------- 1 | id = $id; 19 | } 20 | 21 | public function getId(): UserId 22 | { 23 | return $this->id; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Module/User/Application/Query/User/GetListQuery.php: -------------------------------------------------------------------------------- 1 | id = $id; 19 | $this->name = $name; 20 | $this->email = $email; 21 | } 22 | 23 | public static function createNew(string $name, Email $email): self 24 | { 25 | return new self(UserId::emptyValue(), $name, $email); 26 | } 27 | 28 | public function getId(): UserId 29 | { 30 | return $this->id; 31 | } 32 | 33 | public function getName(): string 34 | { 35 | return $this->name; 36 | } 37 | 38 | public function getEmail(): Email 39 | { 40 | return $this->email; 41 | } 42 | 43 | public function jsonSerialize() 44 | { 45 | return get_object_vars($this); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Module/User/Domain/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Module/User/Infrastructure/Persistence/Doctrine/UserDoctrineRepository.php: -------------------------------------------------------------------------------- 1 | save($user); 19 | } 20 | 21 | public function getById(UserId $id): User 22 | { 23 | $entity = $this->tryGetById($id); 24 | if (!$entity) { 25 | throw NotFoundException::create(); 26 | } 27 | 28 | return $entity; 29 | } 30 | 31 | public function exists(UserId $id): bool 32 | { 33 | return (bool)$this->tryGetById($id); 34 | } 35 | 36 | public function delete(User $user): void 37 | { 38 | $this->deleteEntity($user); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Module/User/Infrastructure/Persistence/Doctrine/UserIdDoctrineType.php: -------------------------------------------------------------------------------- 1 | executeCommand(new CreateCommand( 48 | $request->request->get("name", ""), 49 | $request->request->get("email", "") 50 | )); 51 | return Response::create("", Response::HTTP_CREATED); 52 | } 53 | 54 | /** 55 | * @Route("", name="get_list", methods={"GET"}) 56 | * @SWG\Response( 57 | * response=200, 58 | * description="Returns entity list", 59 | * ) 60 | * @return Response 61 | * @throws Throwable 62 | */ 63 | public function getList(): Response 64 | { 65 | $result = $this->executeQuery(new GetListQuery()); 66 | $entities = iterator_to_array($result); 67 | return $this->jsonResponse($entities); 68 | } 69 | 70 | /** 71 | * @Route("/{id}", name="get_by_id", methods={"GET"}) 72 | * 73 | * @SWG\Response( 74 | * response=200, 75 | * description="Returns selected entity by id", 76 | * ) 77 | * @SWG\Response( 78 | * response=404, 79 | * description="When selected entity doesn't exists", 80 | * ) 81 | * @param int $id 82 | * @return Response 83 | * @throws Throwable 84 | */ 85 | public function getById(int $id): Response 86 | { 87 | $entity = $this->executeQuery(new GetByIdQuery(UserId::create($id))); 88 | return $this->jsonResponse($entity); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "acelaya/doctrine-enum-type": { 3 | "version": "v2.3.0" 4 | }, 5 | "beberlei/assert": { 6 | "version": "v3.2.7" 7 | }, 8 | "doctrine/annotations": { 9 | "version": "1.0", 10 | "recipe": { 11 | "repo": "github.com/symfony/recipes", 12 | "branch": "master", 13 | "version": "1.0", 14 | "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" 15 | }, 16 | "files": [ 17 | "./config/routes/annotations.yaml" 18 | ] 19 | }, 20 | "doctrine/cache": { 21 | "version": "1.10.0" 22 | }, 23 | "doctrine/collections": { 24 | "version": "1.6.4" 25 | }, 26 | "doctrine/common": { 27 | "version": "2.12.0" 28 | }, 29 | "doctrine/dbal": { 30 | "version": "2.10.2" 31 | }, 32 | "doctrine/doctrine-bundle": { 33 | "version": "2.0", 34 | "recipe": { 35 | "repo": "github.com/symfony/recipes", 36 | "branch": "master", 37 | "version": "2.0", 38 | "ref": "a9f2463b9f73efe74482f831f03a204a41328555" 39 | }, 40 | "files": [ 41 | "./config/packages/doctrine.yaml", 42 | "./config/packages/prod/doctrine.yaml", 43 | "./src/Entity/.gitignore", 44 | "./src/Repository/.gitignore" 45 | ] 46 | }, 47 | "doctrine/doctrine-migrations-bundle": { 48 | "version": "1.2", 49 | "recipe": { 50 | "repo": "github.com/symfony/recipes", 51 | "branch": "master", 52 | "version": "1.2", 53 | "ref": "c1431086fec31f17fbcfe6d6d7e92059458facc1" 54 | }, 55 | "files": [ 56 | "./config/packages/doctrine_migrations.yaml", 57 | "./src/Migrations/.gitignore" 58 | ] 59 | }, 60 | "doctrine/event-manager": { 61 | "version": "1.1.0" 62 | }, 63 | "doctrine/inflector": { 64 | "version": "1.3.1" 65 | }, 66 | "doctrine/instantiator": { 67 | "version": "1.3.0" 68 | }, 69 | "doctrine/lexer": { 70 | "version": "1.2.0" 71 | }, 72 | "doctrine/migrations": { 73 | "version": "2.2.1" 74 | }, 75 | "doctrine/orm": { 76 | "version": "v2.7.2" 77 | }, 78 | "doctrine/persistence": { 79 | "version": "1.3.7" 80 | }, 81 | "doctrine/reflection": { 82 | "version": "1.2.1" 83 | }, 84 | "egulias/email-validator": { 85 | "version": "2.1.17" 86 | }, 87 | "exsyst/swagger": { 88 | "version": "v0.4.1" 89 | }, 90 | "hamcrest/hamcrest-php": { 91 | "version": "v2.0.0" 92 | }, 93 | "jdorn/sql-formatter": { 94 | "version": "v1.2.17" 95 | }, 96 | "laminas/laminas-code": { 97 | "version": "3.4.1" 98 | }, 99 | "laminas/laminas-eventmanager": { 100 | "version": "3.2.1" 101 | }, 102 | "laminas/laminas-zendframework-bridge": { 103 | "version": "1.0.3" 104 | }, 105 | "mockery/mockery": { 106 | "version": "1.3.1" 107 | }, 108 | "monolog/monolog": { 109 | "version": "2.0.2" 110 | }, 111 | "myclabs/deep-copy": { 112 | "version": "1.9.5" 113 | }, 114 | "myclabs/php-enum": { 115 | "version": "1.7.6" 116 | }, 117 | "nelmio/api-doc-bundle": { 118 | "version": "3.0", 119 | "recipe": { 120 | "repo": "github.com/symfony/recipes-contrib", 121 | "branch": "master", 122 | "version": "3.0", 123 | "ref": "c8e0c38e1a280ab9e37587a8fa32b251d5bc1c94" 124 | }, 125 | "files": [ 126 | "./config/packages/nelmio_api_doc.yaml", 127 | "./config/routes/nelmio_api_doc.yaml" 128 | ] 129 | }, 130 | "nikic/php-parser": { 131 | "version": "v4.4.0" 132 | }, 133 | "ocramius/package-versions": { 134 | "version": "1.8.0" 135 | }, 136 | "ocramius/proxy-manager": { 137 | "version": "2.8.0" 138 | }, 139 | "phar-io/manifest": { 140 | "version": "1.0.3" 141 | }, 142 | "phar-io/version": { 143 | "version": "2.0.1" 144 | }, 145 | "php": { 146 | "version": "7.4" 147 | }, 148 | "phpdocumentor/reflection-common": { 149 | "version": "2.1.0" 150 | }, 151 | "phpdocumentor/reflection-docblock": { 152 | "version": "5.1.0" 153 | }, 154 | "phpdocumentor/type-resolver": { 155 | "version": "1.1.0" 156 | }, 157 | "phpspec/prophecy": { 158 | "version": "v1.10.3" 159 | }, 160 | "phpunit/php-code-coverage": { 161 | "version": "8.0.1" 162 | }, 163 | "phpunit/php-file-iterator": { 164 | "version": "3.0.1" 165 | }, 166 | "phpunit/php-text-template": { 167 | "version": "2.0.0" 168 | }, 169 | "phpunit/php-timer": { 170 | "version": "3.1.4" 171 | }, 172 | "phpunit/php-token-stream": { 173 | "version": "4.0.0" 174 | }, 175 | "phpunit/phpunit": { 176 | "version": "4.7", 177 | "recipe": { 178 | "repo": "github.com/symfony/recipes", 179 | "branch": "master", 180 | "version": "4.7", 181 | "ref": "00fdb38c318774cd39f475a753028a5e8d25d47c" 182 | }, 183 | "files": [ 184 | "./.env.test", 185 | "./phpunit.xml.dist", 186 | "./tests/bootstrap.php" 187 | ] 188 | }, 189 | "psr/cache": { 190 | "version": "1.0.1" 191 | }, 192 | "psr/container": { 193 | "version": "1.0.0" 194 | }, 195 | "psr/event-dispatcher": { 196 | "version": "1.0.0" 197 | }, 198 | "psr/log": { 199 | "version": "1.1.2" 200 | }, 201 | "sebastian/code-unit-reverse-lookup": { 202 | "version": "2.0.0" 203 | }, 204 | "sebastian/comparator": { 205 | "version": "4.0.0" 206 | }, 207 | "sebastian/diff": { 208 | "version": "4.0.0" 209 | }, 210 | "sebastian/environment": { 211 | "version": "5.1.0" 212 | }, 213 | "sebastian/exporter": { 214 | "version": "4.0.0" 215 | }, 216 | "sebastian/global-state": { 217 | "version": "4.0.0" 218 | }, 219 | "sebastian/object-enumerator": { 220 | "version": "4.0.0" 221 | }, 222 | "sebastian/object-reflector": { 223 | "version": "2.0.0" 224 | }, 225 | "sebastian/recursion-context": { 226 | "version": "4.0.0" 227 | }, 228 | "sebastian/resource-operations": { 229 | "version": "3.0.0" 230 | }, 231 | "sebastian/type": { 232 | "version": "2.0.0" 233 | }, 234 | "sebastian/version": { 235 | "version": "3.0.0" 236 | }, 237 | "sensio/framework-extra-bundle": { 238 | "version": "5.2", 239 | "recipe": { 240 | "repo": "github.com/symfony/recipes", 241 | "branch": "master", 242 | "version": "5.2", 243 | "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b" 244 | }, 245 | "files": [ 246 | "./config/packages/sensio_framework_extra.yaml" 247 | ] 248 | }, 249 | "symfony/asset": { 250 | "version": "v5.0.8" 251 | }, 252 | "symfony/browser-kit": { 253 | "version": "v5.0.8" 254 | }, 255 | "symfony/cache": { 256 | "version": "v5.0.5" 257 | }, 258 | "symfony/cache-contracts": { 259 | "version": "v2.0.1" 260 | }, 261 | "symfony/config": { 262 | "version": "v5.0.5" 263 | }, 264 | "symfony/console": { 265 | "version": "4.4", 266 | "recipe": { 267 | "repo": "github.com/symfony/recipes", 268 | "branch": "master", 269 | "version": "4.4", 270 | "ref": "ea8c0eda34fda57e7d5cd8cbd889e2a387e3472c" 271 | }, 272 | "files": [ 273 | "./bin/console", 274 | "./config/bootstrap.php" 275 | ] 276 | }, 277 | "symfony/css-selector": { 278 | "version": "v5.0.8" 279 | }, 280 | "symfony/dependency-injection": { 281 | "version": "v5.0.5" 282 | }, 283 | "symfony/doctrine-bridge": { 284 | "version": "v5.0.7" 285 | }, 286 | "symfony/dom-crawler": { 287 | "version": "v5.0.8" 288 | }, 289 | "symfony/dotenv": { 290 | "version": "v5.0.5" 291 | }, 292 | "symfony/error-handler": { 293 | "version": "v5.0.5" 294 | }, 295 | "symfony/event-dispatcher": { 296 | "version": "v5.0.5" 297 | }, 298 | "symfony/event-dispatcher-contracts": { 299 | "version": "v2.0.1" 300 | }, 301 | "symfony/filesystem": { 302 | "version": "v5.0.5" 303 | }, 304 | "symfony/finder": { 305 | "version": "v5.0.5" 306 | }, 307 | "symfony/flex": { 308 | "version": "1.0", 309 | "recipe": { 310 | "repo": "github.com/symfony/recipes", 311 | "branch": "master", 312 | "version": "1.0", 313 | "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" 314 | }, 315 | "files": [ 316 | "./.env" 317 | ] 318 | }, 319 | "symfony/framework-bundle": { 320 | "version": "4.4", 321 | "recipe": { 322 | "repo": "github.com/symfony/recipes", 323 | "branch": "master", 324 | "version": "4.4", 325 | "ref": "23ecaccc551fe2f74baf613811ae529eb07762fa" 326 | }, 327 | "files": [ 328 | "./config/bootstrap.php", 329 | "./config/packages/cache.yaml", 330 | "./config/packages/framework.yaml", 331 | "./config/packages/test/framework.yaml", 332 | "./config/routes/dev/framework.yaml", 333 | "./config/services.yaml", 334 | "./public/index.php", 335 | "./src/Controller/.gitignore", 336 | "./src/Kernel.php" 337 | ] 338 | }, 339 | "symfony/google-mailer": { 340 | "version": "4.4", 341 | "recipe": { 342 | "repo": "github.com/symfony/recipes", 343 | "branch": "master", 344 | "version": "4.4", 345 | "ref": "f8fd4ddb9b477510f8f4bce2b9c054ab428c0120" 346 | } 347 | }, 348 | "symfony/http-foundation": { 349 | "version": "v5.0.5" 350 | }, 351 | "symfony/http-kernel": { 352 | "version": "v5.0.5" 353 | }, 354 | "symfony/inflector": { 355 | "version": "v5.0.8" 356 | }, 357 | "symfony/mailer": { 358 | "version": "4.3", 359 | "recipe": { 360 | "repo": "github.com/symfony/recipes", 361 | "branch": "master", 362 | "version": "4.3", 363 | "ref": "15658c2a0176cda2e7dba66276a2030b52bd81b2" 364 | }, 365 | "files": [ 366 | "./config/packages/mailer.yaml" 367 | ] 368 | }, 369 | "symfony/maker-bundle": { 370 | "version": "1.0", 371 | "recipe": { 372 | "repo": "github.com/symfony/recipes", 373 | "branch": "master", 374 | "version": "1.0", 375 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" 376 | } 377 | }, 378 | "symfony/messenger": { 379 | "version": "4.3", 380 | "recipe": { 381 | "repo": "github.com/symfony/recipes", 382 | "branch": "master", 383 | "version": "4.3", 384 | "ref": "8a2675c061737658bed85102e9241c752620e575" 385 | }, 386 | "files": [ 387 | "./config/packages/messenger.yaml" 388 | ] 389 | }, 390 | "symfony/mime": { 391 | "version": "v5.0.5" 392 | }, 393 | "symfony/monolog-bridge": { 394 | "version": "v5.0.8" 395 | }, 396 | "symfony/monolog-bundle": { 397 | "version": "3.3", 398 | "recipe": { 399 | "repo": "github.com/symfony/recipes", 400 | "branch": "master", 401 | "version": "3.3", 402 | "ref": "a89f4cd8a232563707418eea6c2da36acd36a917" 403 | }, 404 | "files": [ 405 | "./config/packages/dev/monolog.yaml", 406 | "./config/packages/prod/monolog.yaml", 407 | "./config/packages/test/monolog.yaml" 408 | ] 409 | }, 410 | "symfony/options-resolver": { 411 | "version": "v5.0.8" 412 | }, 413 | "symfony/orm-pack": { 414 | "version": "v1.0.8" 415 | }, 416 | "symfony/phpunit-bridge": { 417 | "version": "4.3", 418 | "recipe": { 419 | "repo": "github.com/symfony/recipes", 420 | "branch": "master", 421 | "version": "4.3", 422 | "ref": "6d0e35f749d5f4bfe1f011762875275cd3f9874f" 423 | }, 424 | "files": [ 425 | "./.env.test", 426 | "./bin/phpunit", 427 | "./phpunit.xml.dist", 428 | "./tests/bootstrap.php" 429 | ] 430 | }, 431 | "symfony/polyfill-intl-idn": { 432 | "version": "v1.14.0" 433 | }, 434 | "symfony/polyfill-mbstring": { 435 | "version": "v1.14.0" 436 | }, 437 | "symfony/polyfill-php73": { 438 | "version": "v1.14.0" 439 | }, 440 | "symfony/property-info": { 441 | "version": "v5.0.8" 442 | }, 443 | "symfony/routing": { 444 | "version": "4.2", 445 | "recipe": { 446 | "repo": "github.com/symfony/recipes", 447 | "branch": "master", 448 | "version": "4.2", 449 | "ref": "683dcb08707ba8d41b7e34adb0344bfd68d248a7" 450 | }, 451 | "files": [ 452 | "./config/packages/prod/routing.yaml", 453 | "./config/packages/routing.yaml", 454 | "./config/routes.yaml" 455 | ] 456 | }, 457 | "symfony/service-contracts": { 458 | "version": "v2.0.1" 459 | }, 460 | "symfony/stopwatch": { 461 | "version": "v5.0.7" 462 | }, 463 | "symfony/test-pack": { 464 | "version": "v1.0.6" 465 | }, 466 | "symfony/translation-contracts": { 467 | "version": "v2.0.1" 468 | }, 469 | "symfony/twig-bridge": { 470 | "version": "v5.0.8" 471 | }, 472 | "symfony/twig-bundle": { 473 | "version": "5.0", 474 | "recipe": { 475 | "repo": "github.com/symfony/recipes", 476 | "branch": "master", 477 | "version": "5.0", 478 | "ref": "fab9149bbaa4d5eca054ed93f9e1b66cc500895d" 479 | }, 480 | "files": [ 481 | "./config/packages/test/twig.yaml", 482 | "./config/packages/twig.yaml", 483 | "./templates/base.html.twig" 484 | ] 485 | }, 486 | "symfony/twig-pack": { 487 | "version": "v1.0.0" 488 | }, 489 | "symfony/var-dumper": { 490 | "version": "v5.0.5" 491 | }, 492 | "symfony/var-exporter": { 493 | "version": "v5.0.5" 494 | }, 495 | "symfony/yaml": { 496 | "version": "v5.0.5" 497 | }, 498 | "theseer/tokenizer": { 499 | "version": "1.1.3" 500 | }, 501 | "twig/extra-bundle": { 502 | "version": "v3.0.3" 503 | }, 504 | "twig/twig": { 505 | "version": "v3.0.3" 506 | }, 507 | "webimpress/safe-writer": { 508 | "version": "2.0.1" 509 | }, 510 | "webmozart/assert": { 511 | "version": "1.8.0" 512 | }, 513 | "zircote/swagger-php": { 514 | "version": "2.0.15" 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /template_env.local: -------------------------------------------------------------------------------- 1 | # Your gmail account to tests(need enable less secure apps) 2 | MAILER_DSN=gmail://:!@default 3 | # Local part of your gmail username(part before '@') 4 | NOTIFICATION_FROM_EMAIL=+notification@gmail.com 5 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | {% block stylesheets %}{% endblock %} 7 | 8 | 9 | {% block body %}{% endblock %} 10 | {% block javascripts %}{% endblock %} 11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/modules/TodoList/AssignedUserToTaskEmailNotification.twig: -------------------------------------------------------------------------------- 1 | {% block subject %} 2 | You have been assigned to [ {{ task.id }} ] {{ task.name }} 3 | {% endblock %} 4 | 5 | {% block text %} 6 | Hello, {{ user.name }} 7 | You have been assigned to task [ {{ task.id }} ] {{ task.name }} 8 | {% endblock %} 9 | 10 | {% block html %} 11 |

Hello, {{ user.name }}

12 | 13 |

14 | You have been assigned to task [ {{ task.id }} ] {{ task.name }} 15 |

16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | } 12 | -------------------------------------------------------------------------------- /tests/functional/App/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mararok/SymfonyModularMonolith/86593ab3fa9887910ddfcad16b06d9a781d60c8b/tests/functional/App/.gitkeep -------------------------------------------------------------------------------- /tests/integration/App/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mararok/SymfonyModularMonolith/86593ab3fa9887910ddfcad16b06d9a781d60c8b/tests/integration/App/.gitkeep -------------------------------------------------------------------------------- /tests/unit/App/Core/Message/Command/CommandBusTest.php: -------------------------------------------------------------------------------- 1 | expects("dispatch")->with($command)->andReturn(new Envelope($command)); 22 | 23 | $commandBus->handle($command); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/unit/App/Core/Message/Event/EventBusTest.php: -------------------------------------------------------------------------------- 1 | expects("dispatch")->with($event)->andReturn(new Envelope($event)); 21 | 22 | $eventBus->handle($event); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/unit/App/Core/Message/Query/QueryBusTest.php: -------------------------------------------------------------------------------- 1 | expects("dispatch")->with($query)->andReturn($envelope); 24 | 25 | $currentResult = $queryBus->handle($query); 26 | 27 | $this->assertSame($expectedResult, $currentResult); 28 | } 29 | } 30 | --------------------------------------------------------------------------------