├── .env ├── .env.test ├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── README.md ├── bin └── console ├── client ├── .browserslistrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── proxy.backend.conf.json ├── src │ ├── app │ │ ├── _helpers │ │ │ ├── auth.guard.ts │ │ │ ├── error.interceptor.ts │ │ │ └── index.ts │ │ ├── _models │ │ │ ├── comment.ts │ │ │ ├── commentWithPost.ts │ │ │ ├── index.ts │ │ │ ├── newComment.ts │ │ │ ├── newPost.ts │ │ │ ├── page.ts │ │ │ ├── post.ts │ │ │ ├── postHeader.ts │ │ │ ├── tag.ts │ │ │ └── user.ts │ │ ├── _services │ │ │ ├── authentication.service.ts │ │ │ ├── comment.service.ts │ │ │ ├── index.ts │ │ │ ├── post.service.ts │ │ │ └── tag.service.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── edit │ │ │ ├── index.ts │ │ │ ├── post-editor.component.html │ │ │ ├── post-editor.component.scss │ │ │ └── post-editor.component.ts │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ ├── home.component.ts │ │ │ └── index.ts │ │ ├── latest-comments │ │ │ ├── index.ts │ │ │ ├── latest-comments.component.html │ │ │ ├── latest-comments.component.scss │ │ │ └── latest-comments.component.ts │ │ ├── login │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.scss │ │ │ └── login.component.ts │ │ ├── post │ │ │ ├── comment-creator │ │ │ │ ├── comment-creator.component.html │ │ │ │ ├── comment-creator.component.scss │ │ │ │ ├── comment-creator.component.ts │ │ │ │ └── index.ts │ │ │ ├── comment │ │ │ │ ├── comment.component.html │ │ │ │ ├── comment.component.scss │ │ │ │ ├── comment.component.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── post.component.html │ │ │ ├── post.component.scss │ │ │ └── post.component.ts │ │ ├── shared │ │ │ ├── confirm-dialog │ │ │ │ ├── confirm-dialog.component.html │ │ │ │ ├── confirm-dialog.component.ts │ │ │ │ └── index.ts │ │ │ ├── post-action-aware │ │ │ │ ├── index.ts │ │ │ │ └── post-actions-aware.component.ts │ │ │ ├── post-tags │ │ │ │ ├── post-tags.component.html │ │ │ │ ├── post-tags.component.scss │ │ │ │ └── post-tags.component.ts │ │ │ └── posts-list │ │ │ │ ├── index.ts │ │ │ │ ├── posts-list.component.html │ │ │ │ ├── posts-list.component.scss │ │ │ │ └── posts-list.component.ts │ │ └── tags │ │ │ ├── index.ts │ │ │ ├── tags.component.html │ │ │ ├── tags.component.scss │ │ │ └── tags.component.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test.ts ├── tsconfig.app.json ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json ├── clover.xml ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── jwt │ ├── private-test.pem │ └── public-test.pem ├── packages │ ├── cache.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── infrastructure.yaml │ ├── lexik_jwt_authentication.yaml │ ├── messenger.yaml │ ├── module_comments.yaml │ ├── module_posts.yaml │ ├── module_security.yaml │ ├── module_tags.yaml │ ├── prod │ │ └── doctrine.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── sensio_framework_extra.yaml │ ├── test │ │ ├── dama_doctrine_test_bundle.yaml │ │ ├── doctrine.yaml │ │ ├── lexik_jwt_authentication.yaml │ │ └── validator.yaml │ └── validator.yaml ├── preload.php ├── routes │ ├── framework.yaml │ ├── module_comments.yaml │ ├── module_posts.yaml │ ├── module_security.yaml │ └── module_tags.yaml └── services.yaml ├── contracts ├── Comments │ ├── PostBaselinedCommentsIEvent.json │ ├── PostCreatedCommentsIEvent.json │ ├── PostDeletedCommentsIEvent.json │ └── PostUpdatedCommentsIEvent.json ├── Posts │ ├── CommentCreatedPostsIEvent.json │ ├── CommentsBaselinedPostsIEvent.json │ └── UserRenamedPostsIEvent.json └── Tags │ ├── CommentsCountUpdatedTagsIEvent.json │ ├── PostBaselinedTagsIEvent.json │ ├── PostCreatedTagsIEvent.json │ ├── PostDeletedTagsIEvent.json │ ├── PostUpdatedTagsIEvent.json │ └── UserRenamedTagsIEvent.json ├── data ├── comments-test.sqlite ├── comments.sqlite ├── posts-test.sqlite ├── posts.sqlite ├── security-test.sqlite ├── security.sqlite ├── tags-test.sqlite └── tags.sqlite ├── migrations ├── Version20211116075551.php ├── Version20211121154003.php ├── Version20211121154234.php └── Version20211122072414.php ├── phpunit.xml.dist ├── public └── index.php ├── src ├── Infrastructure │ ├── Doctrine │ │ ├── DoctrineEntityManagerAware.php │ │ ├── Migrations │ │ │ └── DoctrineMigration.php │ │ ├── Repository │ │ │ └── DoctrineRepository.php │ │ └── Transactions │ │ │ ├── DoctrineTransaction.php │ │ │ └── DoctrineTransactionFactory.php │ ├── Events │ │ ├── Api │ │ │ ├── ApplicationEventPublisher.php │ │ │ ├── ApplicationEventPublisherInterface.php │ │ │ ├── ApplicationEventSubscriber.php │ │ │ └── EventHandlerReference.php │ │ ├── ApplicationInboundEvent.php │ │ ├── ApplicationOutboundEvent.php │ │ └── EventIdHolder.php │ ├── Pagination │ │ └── Page.php │ ├── Security │ │ ├── JWTBaseListener.php │ │ ├── JWTCommonListener.php │ │ ├── JWTLoggedInUserProvider.php │ │ ├── LoggedInUser.php │ │ ├── LoggedInUserProviderInterface.php │ │ ├── Permission.php │ │ └── SecuredResourceAwareInterface.php │ ├── Services │ │ ├── ParamConverter │ │ │ └── JsonParamConverter.php │ │ └── SqliteForeignKeyEnabler │ │ │ └── SqliteForeignKeyEnabler.php │ ├── Transactions │ │ ├── TransactionFactoryInterface.php │ │ └── TransactionInterface.php │ └── Utils │ │ └── StringUtil.php ├── Kernel.php └── Modules │ ├── Comments │ ├── Api │ │ ├── Command │ │ │ ├── BaselineCommentsCommand.php │ │ │ ├── CreateCommentCommand.php │ │ │ └── Response │ │ │ │ └── CreateCommentCommandResponse.php │ │ ├── CommentsApiInterface.php │ │ ├── Controller │ │ │ ├── CommentsCommandController.php │ │ │ └── CommentsQueryController.php │ │ ├── Event │ │ │ └── Inbound │ │ │ │ ├── PostBaselinedCommentsIEvent.php │ │ │ │ ├── PostCreatedCommentsIEvent.php │ │ │ │ ├── PostDeletedCommentsIEvent.php │ │ │ │ └── PostUpdatedCommentsIEvent.php │ │ └── Query │ │ │ ├── FindCommentsByPostIdQuery.php │ │ │ ├── FindLatestCommentsQuery.php │ │ │ └── Response │ │ │ ├── FindCommentsByPostIdQueryResponse.php │ │ │ ├── FindCommentsPostHeadersQueryResponse.php │ │ │ └── FindLatestCommentsQueryResponse.php │ ├── Domain │ │ ├── CommentsApi.php │ │ ├── Dto │ │ │ ├── CommentDto.php │ │ │ ├── CommentWithPostDto.php │ │ │ ├── CommentsPostHeaderDto.php │ │ │ ├── CreateNewCommentDto.php │ │ │ ├── CreateNewCommentsPostHeaderDto.php │ │ │ ├── DeleteExistingCommentsPostHeaderDto.php │ │ │ ├── UpdateExistingCommentsPostHeaderDto.php │ │ │ └── UpdatedCommentsPostHeadersUserNameDto.php │ │ ├── Event │ │ │ └── Outbound │ │ │ │ ├── CommentCreatedOEvent.php │ │ │ │ ├── CommentsBaselinedOEvent.php │ │ │ │ └── CommentsCountUpdatedOEvent.php │ │ ├── Logic │ │ │ ├── CommentsBaseliner.php │ │ │ ├── CommentsCreator.php │ │ │ ├── CommentsFinder.php │ │ │ ├── CommentsValidator.php │ │ │ ├── PostEventsHandler.php │ │ │ └── PostHeadersFinder.php │ │ ├── Repository │ │ │ ├── CommentsCreationRepositoryInterface.php │ │ │ ├── CommentsDeletionRepositoryInterface.php │ │ │ ├── CommentsFindingRepositoryInterface.php │ │ │ ├── CommentsPostHeadersFindingRepositoryInterface.php │ │ │ └── CommentsPostsEventsHandlingRepositoryInterface.php │ │ └── Transactions │ │ │ └── CommentsTransactionFactoryInterface.php │ └── Persistence │ │ └── Doctrine │ │ ├── Entity │ │ ├── Comment.php │ │ └── CommentPostHeader.php │ │ ├── Repository │ │ ├── DoctrineCommentsCreationRepository.php │ │ ├── DoctrineCommentsDeletionRepository.php │ │ ├── DoctrineCommentsFindingRepository.php │ │ ├── DoctrineCommentsPostHeadersFindingRepository.php │ │ ├── DoctrineCommentsPostsEventsHandlingRepository.php │ │ └── DoctrineCommentsRepository.php │ │ └── Transactions │ │ └── DoctrineCommentsTransactionFactory.php │ ├── Posts │ ├── Api │ │ ├── Command │ │ │ ├── BaselinePostsCommand.php │ │ │ ├── CreatePostCommand.php │ │ │ ├── DeletePostCommand.php │ │ │ ├── Response │ │ │ │ └── CreatePostCommandResponse.php │ │ │ └── UpdatePostCommand.php │ │ ├── Controller │ │ │ ├── PostsCommandController.php │ │ │ └── PostsQueryController.php │ │ ├── Event │ │ │ └── Inbound │ │ │ │ ├── CommentCreatedPostsIEvent.php │ │ │ │ ├── CommentsBaselinedPostsIEvent.php │ │ │ │ └── UserRenamedPostsIEvent.php │ │ ├── PostsApiInterface.php │ │ └── Query │ │ │ ├── FindAllPostsQuery.php │ │ │ ├── FindPostByIdQuery.php │ │ │ └── Response │ │ │ ├── FindPostHeaderQueryResponse.php │ │ │ └── FindPostQueryResponse.php │ ├── Domain │ │ ├── Dto │ │ │ ├── CreateNewPostDto.php │ │ │ ├── DeleteExistingPostDto.php │ │ │ ├── FindExistingPostDto.php │ │ │ ├── FindExistingPostsDto.php │ │ │ ├── PostCommentDto.php │ │ │ ├── PostDto.php │ │ │ ├── PostForBaselineDto.php │ │ │ ├── PostHeaderDto.php │ │ │ ├── UpdateExistingPostDto.php │ │ │ ├── UpdatePostCommentsDto.php │ │ │ └── UpdatedPostsUserNameDto.php │ │ ├── Event │ │ │ └── Outbound │ │ │ │ ├── PostBaselinedOEvent.php │ │ │ │ ├── PostCreatedOEvent.php │ │ │ │ ├── PostDeletedOEvent.php │ │ │ │ └── PostUpdatedOEvent.php │ │ ├── Logic │ │ │ ├── CommentsEventsHandler.php │ │ │ ├── PostUpdater.php │ │ │ ├── PostsBaseliner.php │ │ │ ├── PostsCreator.php │ │ │ ├── PostsFinder.php │ │ │ ├── PostsRemover.php │ │ │ ├── PostsValidator.php │ │ │ └── SecurityEventsHandler.php │ │ ├── PostsApi.php │ │ ├── Repository │ │ │ ├── PostsCommentsEventHandlingRepositoryInterface.php │ │ │ ├── PostsCreationRepositoryInterface.php │ │ │ ├── PostsDeletionRepositoryInterface.php │ │ │ ├── PostsFindingRepositoryInterface.php │ │ │ ├── PostsSecurityEventsHandlingRepositoryInterface.php │ │ │ └── PostsUpdatingRepositoryInterface.php │ │ ├── Security │ │ │ └── PostsVoter.php │ │ └── Transactions │ │ │ └── PostTransactionFactoryInterface.php │ └── Persistence │ │ └── Doctrine │ │ ├── Entity │ │ ├── Post.php │ │ └── PostComments.php │ │ ├── Repository │ │ ├── DoctrinePostsCommentsEventHandlingRepository.php │ │ ├── DoctrinePostsCreationRepository.php │ │ ├── DoctrinePostsDeletionRepository.php │ │ ├── DoctrinePostsFindingRepository.php │ │ ├── DoctrinePostsRepository.php │ │ ├── DoctrinePostsSecurityEventsHandlingRepository.php │ │ └── DoctrinePostsUpdatingRepository.php │ │ └── Transactions │ │ └── DoctrinePostTransactionFactory.php │ ├── Security │ ├── Api │ │ ├── Command │ │ │ ├── ChangeUserPasswordCommand.php │ │ │ ├── CreateUserCommand.php │ │ │ ├── RenameUserCommand.php │ │ │ └── Response │ │ │ │ └── CreateUserCommandResponse.php │ │ ├── Controller │ │ │ └── LogoutController.php │ │ └── SecurityApiInterface.php │ ├── Domain │ │ ├── Dto │ │ │ ├── ChangeExistingUserPasswordDto.php │ │ │ ├── CreateNewUserDto.php │ │ │ ├── CreateNewUserPostHeaderDto.php │ │ │ ├── DeleteExistingUserPostHeaderDto.php │ │ │ ├── RenameExistingUserDto.php │ │ │ ├── UpdateExistingUserPostHeaderDto.php │ │ │ ├── UpdatePostsCommentsCountDto.php │ │ │ └── UserPostHeaderDto.php │ │ ├── Event │ │ │ └── Outbound │ │ │ │ └── UserRenamedOEvent.php │ │ ├── Logic │ │ │ ├── JWTSecurityListener.php │ │ │ ├── PasswordHasher.php │ │ │ ├── SecurityValidator.php │ │ │ ├── UserCreator.php │ │ │ └── UserUpdater.php │ │ ├── Provider │ │ │ └── SymfonyDatabaseLoggedInUserProvider.php │ │ ├── Repository │ │ │ ├── UserCreationRepositoryInterface.php │ │ │ ├── UserFindingRepositoryInterface.php │ │ │ └── UserUpdatingRepositoryInterface.php │ │ ├── SecurityApi.php │ │ └── Transactions │ │ │ └── SecurityTransactionFactoryInterface.php │ └── Persistence │ │ └── Doctrine │ │ ├── Entity │ │ └── User.php │ │ ├── Repository │ │ ├── DoctrineSecurityRepository.php │ │ ├── DoctrineUserCreationRepository.php │ │ ├── DoctrineUserFindingRepository.php │ │ └── DoctrineUserUpdatingRepository.php │ │ └── Transactions │ │ └── DoctrineSecurityTransactionFactory.php │ └── Tags │ ├── Api │ ├── Controller │ │ └── TagsQueryController.php │ ├── Event │ │ └── Inbound │ │ │ ├── CommentsCountUpdatedTagsIEvent.php │ │ │ ├── PostBaselinedTagsIEvent.php │ │ │ ├── PostCreatedTagsIEvent.php │ │ │ ├── PostDeletedTagsIEvent.php │ │ │ ├── PostUpdatedTagsIEvent.php │ │ │ └── UserRenamedTagsIEvent.php │ ├── Query │ │ ├── FindPostsByTagQuery.php │ │ └── Response │ │ │ ├── FindPostsByTagQueryResponse.php │ │ │ ├── FindTagsPostHeadersQueryResponse.php │ │ │ └── FindTagsQueryResponse.php │ └── TagsApiInterface.php │ ├── Domain │ ├── Dto │ │ ├── CreateNewTagsPostHeaderDto.php │ │ ├── DeleteExistingTagsPostHeaderDto.php │ │ ├── TagDto.php │ │ ├── TagsPostHeaderDto.php │ │ ├── UpdateExistingTagsPostHeaderDto.php │ │ ├── UpdatePostsCommentsCountDto.php │ │ └── UpdatedTagsPostHeadersUserNameDto.php │ ├── Logic │ │ ├── CommentsEventsHandler.php │ │ ├── PostHeadersFinder.php │ │ ├── PostsEventsHandler.php │ │ ├── SecurityEventsHandler.php │ │ ├── TagsFinder.php │ │ └── TagsUpdater.php │ ├── Repository │ │ ├── TagsCommentsEventHandlingRepositoryInterface.php │ │ ├── TagsDeletingRepositoryInterface.php │ │ ├── TagsFindingRepositoryInterface.php │ │ ├── TagsPostEventsHandlingRepositoryInterface.php │ │ ├── TagsPostHeadersFindingRepositoryInterface.php │ │ ├── TagsSecurityEventsHandlingRepositoryInterface.php │ │ └── TagsUpdatingRepositoryInterface.php │ ├── TagsApi.php │ └── Transactions │ │ └── TagsTransactionFactoryInterface.php │ └── Persistence │ └── Doctrine │ ├── Entity │ ├── Tag.php │ ├── TagPost.php │ └── TagPostHeader.php │ ├── Repository │ ├── DoctrineTagsCommentsEventHandlingRepository.php │ ├── DoctrineTagsDeletingRepository.php │ ├── DoctrineTagsFindingRepository.php │ ├── DoctrineTagsPostEventsHandlingRepository.php │ ├── DoctrineTagsPostHeadersFindingRepository.php │ ├── DoctrineTagsRepository.php │ ├── DoctrineTagsSecurityEventsHandlingRepository.php │ └── DoctrineTagsUpdatingRepository.php │ └── Transactions │ └── DoctrineTagsTransactionFactory.php ├── symfony.lock └── tests ├── Infrastructure ├── Events │ ├── Api │ │ ├── PublishSubscribeEventsSpec.php │ │ ├── TestEventSubscriber.php │ │ └── TestMessageBus.php │ ├── ApplicationInboundEventSpec.php │ ├── EventIdHolderSpec.php │ ├── TestApplicationInboundErrorEvent.php │ ├── TestApplicationInboundEvent.php │ ├── TestApplicationOutboundErrorEvent.php │ └── TestApplicationOutboundEvent.php └── Utils │ └── Unit │ └── StringUtilSpec.php ├── Modules ├── Comments │ ├── Contracts │ │ ├── Inbound │ │ │ ├── PostBaselinedIEventContract.php │ │ │ ├── PostCreatedIEventContract.php │ │ │ ├── PostDeletedIEventContract.php │ │ │ └── PostUpdatedIEventContract.php │ │ └── Outbound │ │ │ ├── CommentCreatedOEventContract.php │ │ │ ├── CommentsBaselinedOEventContract.php │ │ │ └── CommentsCountUpdatedOEventContract.php │ ├── Integration │ │ ├── CommentsIT.php │ │ └── Http │ │ │ └── CommentsHttpTrait.php │ └── Unit │ │ ├── CommentsCreationSpec.php │ │ ├── CommentsFindingSpec.php │ │ ├── CommentsSpec.php │ │ ├── PostEventsCommentsHandlingSpec.php │ │ ├── Repository │ │ ├── InMemoryComment.php │ │ ├── InMemoryCommentPostHeader.php │ │ └── InMemoryCommentsRepository.php │ │ └── Transaction │ │ └── InMemoryCommentsTransactionFactory.php ├── Posts │ ├── Contracts │ │ ├── Inbound │ │ │ ├── CommentCreatedPostsIEventContract.php │ │ │ ├── CommentsBaselinedPostsIEventContract.php │ │ │ └── UserRenamedPostsIEventContract.php │ │ └── Outbound │ │ │ ├── PostBaselinedOEventContract.php │ │ │ ├── PostCreatedOEventContract.php │ │ │ ├── PostDeletedOEventContract.php │ │ │ └── PostUpdatedOEventContract.php │ ├── Integration │ │ ├── Http │ │ │ └── PostsHttpTrait.php │ │ └── PostIT.php │ └── Unit │ │ ├── CommentsEvensPostsHandlingSpec.php │ │ ├── PostCreationSpec.php │ │ ├── PostDeletionSpec.php │ │ ├── PostFindingSpec.php │ │ ├── PostSecurityEvensPostsHandlingSpec.php │ │ ├── PostUpdatingSpec.php │ │ ├── PostsSpec.php │ │ ├── Repository │ │ ├── InMemoryPost.php │ │ └── InMemoryPostsRepository.php │ │ └── Transaction │ │ └── InMemoryPostTransactionFactory.php ├── Security │ ├── Contracts │ │ └── Outbound │ │ │ └── UserRenamedOEventContract.php │ └── Integration │ │ ├── Http │ │ └── SecurityHttpTrait.php │ │ ├── SecurityIT.php │ │ └── SecurityIntegrationTest.php └── Tags │ ├── Contracts │ └── Inbound │ │ ├── CommentsCountUpdatedTagsIEventContract.php │ │ ├── PostBaselinedIEventContract.php │ │ ├── PostCreatedIEventContract.php │ │ ├── PostDeletedIEventContract.php │ │ ├── PostUpdatedIEventContract.php │ │ └── UserRenamedTagsIEventContract.php │ ├── Integration │ ├── Http │ │ └── TagsHttpTrait.php │ └── TagsIT.php │ └── Unit │ ├── CommentsEventsTagsHandlingSpec.php │ ├── PostEventsTagsHandlingSpec.php │ ├── Repository │ ├── InMemoryTag.php │ ├── InMemoryTagPostHeader.php │ └── InMemoryTagsRepository.php │ ├── TagsSecurityEventsTagsHandlingSpec.php │ ├── TagsSpec.php │ ├── TagsUpdateAndFindingSpec.php │ └── Transaction │ └── InMemoryTagsTransactionFactory.php ├── TestUtils ├── Contracts │ ├── ApplicationEventContractLoader.php │ ├── ApplicationInboundEventContract.php │ └── ApplicationOutboundEventContract.php ├── Events │ └── InMemoryEventPublisher.php ├── IntegrationTest.php ├── Security │ └── InMemoryLoggedInUserProvider.php ├── SerializationTrait.php └── Transaction │ ├── InMemoryTransaction.php │ └── InMemoryTransactionFactory.php └── bootstrap.php /.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 | PANTHER_APP_ENV=panther 6 | PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots 7 | DATABASE_URL_POSTS_TEST=sqlite:///%kernel.project_dir%/data/posts-test.sqlite 8 | DATABASE_URL_COMMENTS_TEST=sqlite:///%kernel.project_dir%/data/comments-test.sqlite 9 | DATABASE_URL_TAGS_TEST=sqlite:///%kernel.project_dir%/data/tags-test.sqlite 10 | DATABASE_URL_SECURITY_TEST=sqlite:///%kernel.project_dir%/data/security-test.sqlite -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cache and logs (Symfony2) 2 | /app/cache/* 3 | /app/logs/* 4 | !app/cache/.gitkeep 5 | !app/logs/.gitkeep 6 | 7 | # Email spool folder 8 | /app/spool/* 9 | 10 | # Cache, session files and logs (Symfony3) 11 | /var/cache/* 12 | /var/logs/* 13 | /var/sessions/* 14 | !var/cache/.gitkeep 15 | !var/logs/.gitkeep 16 | !var/sessions/.gitkeep 17 | 18 | # Logs (Symfony4) 19 | /var/log/* 20 | !var/log/.gitkeep 21 | 22 | # Parameters 23 | /app/config/parameters.yml 24 | /app/config/parameters.ini 25 | 26 | # Managed by Composer 27 | /app/bootstrap.php.cache 28 | /var/bootstrap.php.cache 29 | /bin/* 30 | !bin/console 31 | !bin/symfony_requirements 32 | /vendor/ 33 | /var/ 34 | /.idea/ 35 | *.iml 36 | 37 | # Assets and user uploads 38 | /web/bundles/ 39 | /web/uploads/ 40 | 41 | # PHPUnit 42 | /app/phpunit.xml 43 | /phpunit.xml 44 | 45 | # Build data 46 | /build/ 47 | 48 | # Composer PHAR 49 | /composer.phar 50 | 51 | # Backup entities generated with doctrine:generate:entities command 52 | **/Entity/*~ 53 | 54 | # Embedded web-server pid file 55 | /.web-server-pid 56 | 57 | ###> symfony/phpunit-bridge ### 58 | .phpunit.result.cache 59 | /phpunit.xml 60 | ###< symfony/phpunit-bridge ### 61 | 62 | ###> phpunit/phpunit ### 63 | /phpunit.xml 64 | .phpunit.result.cache 65 | ###< phpunit/phpunit ### 66 | 67 | ###> lexik/jwt-authentication-bundle ### 68 | /config/jwt/private.pem 69 | /config/jwt/public.pem 70 | ###< lexik/jwt-authentication-bundle ### 71 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | environment: 3 | php: 4 | version: '8.0.9' 5 | nodes: 6 | analysis: 7 | tests: 8 | override: 9 | - php-scrutinizer-run 10 | coverage: 11 | tests: 12 | override: 13 | - command: ./vendor/bin/phpunit --coverage-clover clover.xml 14 | coverage: 15 | file: clover.xml 16 | format: clover -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - '8.0.9' 4 | install: 5 | - composer install --dev 6 | script: 7 | - ./vendor/bin/phpunit tests --test-suffix=Spec.php 8 | - ./vendor/bin/phpunit tests --test-suffix=IT.php 9 | - ./vendor/bin/phpunit tests --test-suffix=Contract.php -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Slawomir Dymitrow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | , next: HttpHandler): Observable> { 13 | return next.handle(request).pipe(catchError(err => { 14 | if ([401].includes(err.status) && this.authenticationService.userValue) { 15 | // auto logout if 401 response returned from api 16 | this.authenticationService.logout(); 17 | } 18 | 19 | const error = (err && err.error && err.error.message) || err.statusText; 20 | console.error(err); 21 | return throwError(error); 22 | })) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/src/app/_helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard'; 2 | export * from './error.interceptor'; 3 | -------------------------------------------------------------------------------- /client/src/app/_models/comment.ts: -------------------------------------------------------------------------------- 1 | export class Comment { 2 | id: string; 3 | author: string; 4 | body: string; 5 | createdAt: string[]; 6 | parentId: string | null; 7 | postId: string; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/app/_models/commentWithPost.ts: -------------------------------------------------------------------------------- 1 | export class CommentWithPost { 2 | id: string; 3 | author: string; 4 | body: string; 5 | createdAt: string; 6 | parentId: string | null; 7 | postId: string; 8 | postTitle: string; 9 | postTags: string[]; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/app/_models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; 2 | export * from './post'; 3 | export * from './postHeader'; 4 | export * from './tag'; 5 | export * from './comment'; 6 | export * from './page'; 7 | export * from './commentWithPost'; 8 | export * from './newComment'; 9 | export * from './newPost'; 10 | -------------------------------------------------------------------------------- /client/src/app/_models/newComment.ts: -------------------------------------------------------------------------------- 1 | export class NewComment { 2 | postId: string; 3 | author: string; 4 | body: string; 5 | parentId: string | null; 6 | 7 | 8 | constructor(postId: string, author: string, body: string, parentId: string | null) { 9 | this.postId = postId; 10 | this.author = author; 11 | this.body = body; 12 | this.parentId = parentId; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/src/app/_models/newPost.ts: -------------------------------------------------------------------------------- 1 |  2 | export class NewPost { 3 | title: string; 4 | body: string; 5 | tags: string[]; 6 | 7 | 8 | constructor(title: string, body: string, tags: string[]) { 9 | this.title = title; 10 | this.body = body; 11 | this.tags = tags; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/app/_models/page.ts: -------------------------------------------------------------------------------- 1 | export class Page { 2 | data: Array; 3 | count: number; 4 | pageSize: number; 5 | pageNo: number; 6 | } 7 | -------------------------------------------------------------------------------- /client/src/app/_models/post.ts: -------------------------------------------------------------------------------- 1 | import {Comment} from "@app/_models/comment"; 2 | 3 | export class Post { 4 | id: string; 5 | title: string; 6 | body: string; 7 | comments: Array; 8 | createdById: string; 9 | createdByName: string; 10 | createdAt: string; 11 | tags: string[]; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/app/_models/postHeader.ts: -------------------------------------------------------------------------------- 1 | export class PostHeader { 2 | id: string; 3 | title: string; 4 | summary: string; 5 | commentsCount: number; 6 | createdById: string; 7 | createdByName: string; 8 | createdAt: string; 9 | tags: string[]; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/app/_models/tag.ts: -------------------------------------------------------------------------------- 1 | export class Tag { 2 | tag: string; 3 | postsCount: number; 4 | } 5 | -------------------------------------------------------------------------------- /client/src/app/_models/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | id: number; 3 | login: string; 4 | roles: string[]; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/app/_services/comment.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | 4 | import {environment} from '@environments/environment'; 5 | import {CommentWithPost, NewComment, Page} from '@app/_models'; 6 | import {map} from "rxjs/operators"; 7 | 8 | @Injectable({providedIn: 'root'}) 9 | export class CommentService { 10 | constructor(private http: HttpClient) { 11 | } 12 | 13 | getLatestComments(pageNo: number) { 14 | return this.http.get>(`${environment.apiUrl}/api/comments/?pageNo=` + pageNo); 15 | } 16 | 17 | createComment(comment: NewComment) { 18 | return this.http.post(`${environment.apiUrl}/api/comments/`, comment); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /client/src/app/_services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authentication.service'; 2 | export * from './tag.service'; 3 | export * from './post.service'; 4 | export * from './comment.service'; 5 | -------------------------------------------------------------------------------- /client/src/app/_services/post.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | 4 | import {environment} from '@environments/environment'; 5 | import {NewPost, Page, Post, PostHeader} from '@app/_models'; 6 | 7 | @Injectable({providedIn: 'root'}) 8 | export class PostService { 9 | constructor(private http: HttpClient) { 10 | } 11 | 12 | getPosts(pageNo: number) { 13 | return this.http.get>(`${environment.apiUrl}/api/posts/?pageNo=` + pageNo); 14 | } 15 | 16 | getPost(postId: string) { 17 | return this.http.get(`${environment.apiUrl}/api/posts/` + postId); 18 | } 19 | 20 | updatePost(postId: string, newPost: NewPost) { 21 | return this.http.put(`${environment.apiUrl}/api/admin/posts/` + postId, newPost); 22 | } 23 | 24 | createPost(newPost: NewPost) { 25 | return this.http.post(`${environment.apiUrl}/api/admin/posts/`, newPost); 26 | } 27 | 28 | deletePost(postId: string) { 29 | return this.http.delete(`${environment.apiUrl}/api/admin/posts/` + postId); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/src/app/_services/tag.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | 4 | import {environment} from '@environments/environment'; 5 | import {Page, PostHeader, Tag} from '@app/_models'; 6 | 7 | @Injectable({providedIn: 'root'}) 8 | export class TagService { 9 | constructor(private http: HttpClient) { 10 | } 11 | 12 | getPostsByTag(tag: string, pageNo: number) { 13 | return this.http.get>(`${environment.apiUrl}/api/tags/` + tag + '?pageNo=' + pageNo); 14 | } 15 | 16 | getTags() { 17 | return this.http.get(`${environment.apiUrl}/api/tags/`); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | 4 | import {HomeComponent} from './home'; 5 | import {LoginComponent} from './login'; 6 | import {AuthGuard} from './_helpers'; 7 | import {PostComponent} from "@app/post/post.component"; 8 | import {TagsComponent} from "@app/tags"; 9 | import {PostEditorComponent} from "@app/edit/post-editor.component"; 10 | import {LatestCommentsComponent} from "@app/latest-comments/latest-comments.component"; 11 | 12 | const routes: Routes = [ 13 | {path: '', component: HomeComponent}, 14 | {path: 'post/:postId', component: PostComponent}, 15 | {path: 'tags/:tag', component: TagsComponent}, 16 | {path: 'latest-comments', component: LatestCommentsComponent, canActivate: [AuthGuard]}, 17 | {path: 'create', component: PostEditorComponent, canActivate: [AuthGuard]}, 18 | {path: 'edit/:postId', component: PostEditorComponent, canActivate: [AuthGuard]}, 19 | {path: 'login', component: LoginComponent}, 20 | 21 | // otherwise redirect to home 22 | {path: '**', redirectTo: ''} 23 | ]; 24 | 25 | @NgModule({ 26 | imports: [RouterModule.forRoot(routes, {relativeLinkResolution: 'legacy'})], 27 | exports: [RouterModule] 28 | }) 29 | export class AppRoutingModule { 30 | } 31 | -------------------------------------------------------------------------------- /client/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav-icon { 2 | margin-right: 5px; 3 | } 4 | 5 | .title { 6 | cursor: pointer; 7 | } 8 | 9 | .user-container { 10 | display: flex; 11 | } 12 | 13 | .user-name { 14 | font-size: medium; 15 | } 16 | 17 | .toolbar-spacer { 18 | flex: 1 1 auto; 19 | } 20 | 21 | .user-icon { 22 | margin-top: 3px; 23 | } 24 | -------------------------------------------------------------------------------- /client/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { AuthenticationService } from './_services'; 4 | import { User } from './_models'; 5 | 6 | @Component({ selector: 'app', templateUrl: 'app.component.html', styleUrls: ['app.component.scss'] }) 7 | export class AppComponent { 8 | user: User; 9 | 10 | constructor(private authenticationService: AuthenticationService) { 11 | this.authenticationService.user.subscribe(x => this.user = x); 12 | } 13 | 14 | logout() { 15 | this.authenticationService.logout(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/app/edit/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post-editor.component'; 2 | -------------------------------------------------------------------------------- /client/src/app/edit/post-editor.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | justify-content: center; 4 | margin: 100px 0px; 5 | } 6 | 7 | .mat-form-field { 8 | width: 100%; 9 | min-width: 800px; 10 | } 11 | 12 | mat-card-title, 13 | mat-card-content { 14 | display: flex; 15 | justify-content: center; 16 | } 17 | 18 | .error { 19 | padding: 16px; 20 | width: 300px; 21 | color: white; 22 | background-color: red; 23 | } 24 | 25 | .editor-footer { 26 | display: flex; 27 | justify-content: flex-end; 28 | } 29 | 30 | .save-button { 31 | margin: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /client/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | 
2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 |

There are no Posts on this Blog

14 |
15 |
16 |
17 |
18 | 19 | 20 | #{{tag.tag}} ({{tag.postsCount}}) 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /client/src/app/home/home.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 60%; 3 | min-width: 700px; 4 | justify-content: center; 5 | margin: auto; 6 | display: flex; 7 | } 8 | 9 | .posts { 10 | width: 80%; 11 | min-width: 700px; 12 | justify-content: center; 13 | margin: auto; 14 | display: grid; 15 | } 16 | 17 | .tags { 18 | width: 15%; 19 | 20 | } 21 | 22 | .tags .mat-list-item { 23 | font-size: small; 24 | cursor: pointer; 25 | color: grey; 26 | } 27 | 28 | .mat-paginator-container { 29 | margin: auto; 30 | width: 60%; 31 | min-width: 700px; 32 | } 33 | 34 | 35 | .post-card { 36 | margin-bottom: 10px; 37 | min-width: 700px; 38 | } 39 | 40 | .post-cards { 41 | margin-top: 20px; 42 | } 43 | 44 | .post-title { 45 | display: flex; 46 | } 47 | 48 | .post-title-spacer { 49 | flex: 1 1 auto; 50 | } 51 | 52 | .post-footer { 53 | margin: 20px 10px 10px; 54 | font-size: small; 55 | color: gray; 56 | display: flex; 57 | } 58 | 59 | .post-subtitle { 60 | font-size: smaller; 61 | } 62 | 63 | .post-actions { 64 | font-size: xx-small; 65 | } 66 | -------------------------------------------------------------------------------- /client/src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; -------------------------------------------------------------------------------- /client/src/app/latest-comments/index.ts: -------------------------------------------------------------------------------- 1 | export * from './latest-comments.component'; 2 | -------------------------------------------------------------------------------- /client/src/app/latest-comments/latest-comments.component.html: -------------------------------------------------------------------------------- 1 |  2 | 3 |

Latest Comments on the Blog

4 | 5 | 6 | {{comment.postTitle}} 7 | 8 | 9 | 10 | Commented by {{comment.author}} 11 | at {{comment.createdAt}} 12 | 13 | 14 | 15 | 19 | 20 | 21 |
22 | 23 |

There are no Posts with Comments on this Blog

24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /client/src/app/latest-comments/latest-comments.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {first} from 'rxjs/operators'; 3 | 4 | import {CommentWithPost, Page, User} from '@app/_models'; 5 | import {PageEvent} from "@angular/material/paginator"; 6 | import {AuthenticationService, CommentService} from "@app/_services"; 7 | 8 | @Component({templateUrl: 'latest-comments.component.html', styleUrls: ['latest-comments.component.scss']}) 9 | export class LatestCommentsComponent { 10 | 11 | loading = false; 12 | comments: Page; 13 | user: User; 14 | pageNo: number = 1; 15 | 16 | constructor(private authenticationService: AuthenticationService, 17 | private commentService: CommentService) { 18 | this.authenticationService.user.subscribe(x => this.user = x); 19 | } 20 | 21 | 22 | ngOnInit() { 23 | this.loading = true; 24 | this.loadPosts(this.pageNo); 25 | } 26 | 27 | private loadPosts(pageNo: number) { 28 | this.commentService.getLatestComments(pageNo).pipe(first()).subscribe(posts => { 29 | this.loading = false; 30 | this.comments = posts; 31 | }); 32 | } 33 | 34 | public getPaginatorData(event: PageEvent): PageEvent { 35 | this.pageNo = event.pageIndex + 1; 36 | this.loadPosts(this.pageNo); 37 | return event; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/src/app/login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; -------------------------------------------------------------------------------- /client/src/app/login/login.component.html: -------------------------------------------------------------------------------- 1 |  2 | Login 3 | 4 | 5 | 6 | 7 |
8 |

9 | 10 | 11 | 12 |

13 | 14 |

15 | 16 | 17 | 18 |

19 | 20 |

21 | {{ error }} 22 |

23 | 24 |
25 | 26 |
27 | 28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /client/src/app/login/login.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | justify-content: center; 4 | margin: 100px 0px; 5 | } 6 | 7 | .mat-form-field { 8 | width: 100%; 9 | min-width: 300px; 10 | } 11 | 12 | mat-card-title, 13 | mat-card-content { 14 | display: flex; 15 | justify-content: center; 16 | } 17 | 18 | .error { 19 | padding: 16px; 20 | width: 300px; 21 | color: white; 22 | background-color: red; 23 | } 24 | 25 | .button { 26 | display: flex; 27 | justify-content: flex-end; 28 | } 29 | 30 | .credentials { 31 | display: flex; 32 | justify-content: center; 33 | } 34 | -------------------------------------------------------------------------------- /client/src/app/post/comment-creator/comment-creator.component.html: -------------------------------------------------------------------------------- 1 | 2 | Post new Comment 3 | 4 |
5 |

6 | 7 | 8 | 9 |

10 | 11 |

12 | 13 | 20 | 21 |

22 | 23 |

24 | {{ error }} 25 |

26 |
27 |
28 |
29 | 30 | 31 |
32 | 33 | 34 |
35 | -------------------------------------------------------------------------------- /client/src/app/post/comment-creator/comment-creator.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eXsio/php-symfony-arch/669ea3ffa016c6baf63b8eaf87ff53de6bc6a62b/client/src/app/post/comment-creator/comment-creator.component.scss -------------------------------------------------------------------------------- /client/src/app/post/comment-creator/index.ts: -------------------------------------------------------------------------------- 1 | export * from './comment-creator.component'; 2 | -------------------------------------------------------------------------------- /client/src/app/post/comment/comment.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Commented by {{comment.author}} at {{comment.createdAt['date']}} 4 | 5 | 6 | {{comment.body}} 7 | 8 | 9 | 10 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/src/app/post/comment/comment.component.scss: -------------------------------------------------------------------------------- 1 | .comment-footer { 2 | display: flex; 3 | color: gray; 4 | justify-content: flex-end; 5 | } 6 | 7 | .comment { 8 | margin-top: 10px; 9 | } 10 | -------------------------------------------------------------------------------- /client/src/app/post/comment/comment.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from "@angular/core"; 2 | import {Comment} from "@app/_models/comment"; 3 | import {Post} from "@app/_models"; 4 | 5 | @Component({selector: 'comment', templateUrl: 'comment.component.html', styleUrls: ['comment.component.scss']}) 6 | export class CommentComponent { 7 | 8 | @Input("comment") comment: Comment; 9 | @Input("postId") postId: string; 10 | @Input("onReply") onReply: (postId: string, commentId: string) => void; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /client/src/app/post/comment/index.ts: -------------------------------------------------------------------------------- 1 | export * from './comment.component'; 2 | -------------------------------------------------------------------------------- /client/src/app/post/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post.component'; 2 | -------------------------------------------------------------------------------- /client/src/app/post/post.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 60%; 3 | justify-content: center; 4 | margin: auto; 5 | display: grid; 6 | } 7 | 8 | .post-card { 9 | margin-top: 20px; 10 | min-width: 700px; 11 | } 12 | 13 | .post-cards { 14 | margin-top: 20px; 15 | } 16 | 17 | .post-title { 18 | display: flex; 19 | } 20 | 21 | .post-title-spacer { 22 | flex: 1 1 auto; 23 | } 24 | 25 | .post-footer { 26 | margin: 20px 10px 10px; 27 | font-size: small; 28 | color: gray; 29 | display: flex; 30 | } 31 | 32 | .post-subtitle { 33 | font-size: smaller; 34 | } 35 | 36 | .post-actions { 37 | font-size: xx-small; 38 | } 39 | 40 | .created-by { 41 | cursor: pointer; 42 | } 43 | .mat-tree-node { 44 | display: flex; 45 | } 46 | 47 | .comments-tree-invisible { 48 | display: none; 49 | } 50 | 51 | .comments-tree ul, 52 | .comments-tree li { 53 | margin-top: 0; 54 | margin-bottom: 0; 55 | list-style-type: none; 56 | } 57 | 58 | .comments-tree .mat-nested-tree-node div[role=group] { 59 | padding-left: 40px; 60 | } 61 | 62 | .commentstree div[role=group] > .mat-tree-node { 63 | padding-left: 40px; 64 | } 65 | 66 | .invisible { 67 | visibility: hidden; 68 | } 69 | 70 | .comment { 71 | width: 90%; 72 | } 73 | 74 | .post-comments h5 { 75 | color: grey; 76 | } 77 | 78 | .post-tags .mat-button { 79 | color: grey; 80 | font-size: 12px; 81 | } 82 | -------------------------------------------------------------------------------- /client/src/app/shared/confirm-dialog/confirm-dialog.component.html: -------------------------------------------------------------------------------- 1 |

2 | {{title}} 3 |

4 | 5 |
6 |

{{message}}

7 |
8 | 9 |
10 | 11 | 12 |
13 | -------------------------------------------------------------------------------- /client/src/app/shared/confirm-dialog/confirm-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject, OnInit} from '@angular/core'; 2 | import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; 3 | 4 | @Component({ 5 | selector: 'app-confirm-dialog', 6 | templateUrl: './confirm-dialog.component.html' 7 | }) 8 | export class ConfirmDialogComponent implements OnInit { 9 | title: string; 10 | message: string; 11 | 12 | constructor(public dialogRef: MatDialogRef, 13 | @Inject(MAT_DIALOG_DATA) public data: ConfirmDialogModel) { 14 | // Update view with given values 15 | this.title = data.title; 16 | this.message = data.message; 17 | } 18 | 19 | ngOnInit() { 20 | } 21 | 22 | onConfirm(): void { 23 | // Close the dialog, return true 24 | this.dialogRef.close(true); 25 | } 26 | 27 | onDismiss(): void { 28 | // Close the dialog, return false 29 | this.dialogRef.close(false); 30 | } 31 | } 32 | 33 | /** 34 | * Class to represent confirm dialog model. 35 | * 36 | * It has been kept here to keep it as part of shared component. 37 | */ 38 | export class ConfirmDialogModel { 39 | 40 | constructor(public title: string, public message: string) { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/src/app/shared/confirm-dialog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './confirm-dialog.component'; 2 | -------------------------------------------------------------------------------- /client/src/app/shared/post-action-aware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './post-actions-aware.component'; 2 | -------------------------------------------------------------------------------- /client/src/app/shared/post-tags/post-tags.component.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /client/src/app/shared/post-tags/post-tags.component.scss: -------------------------------------------------------------------------------- 1 | .post-tags .mat-button { 2 | color: grey; 3 | font-size: 12px; 4 | } 5 | -------------------------------------------------------------------------------- /client/src/app/shared/post-tags/post-tags.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from "@angular/core"; 2 | 3 | @Component({selector: 'post-tags', templateUrl: 'post-tags.component.html', styleUrls: ['post-tags.component.scss']}) 4 | export class PostTagsComponent { 5 | 6 | @Input("tags") tags: string[]; 7 | } 8 | -------------------------------------------------------------------------------- /client/src/app/shared/posts-list/index.ts: -------------------------------------------------------------------------------- 1 | export * from './posts-list.component'; 2 | -------------------------------------------------------------------------------- /client/src/app/shared/posts-list/posts-list.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .post-card { 3 | margin-bottom: 10px; 4 | min-width: 700px; 5 | } 6 | 7 | .post-cards { 8 | margin-top: 20px; 9 | } 10 | 11 | .post-title { 12 | display: flex; 13 | cursor: pointer; 14 | } 15 | 16 | .post-summary { 17 | cursor: pointer; 18 | } 19 | 20 | .post-comments { 21 | cursor: pointer; 22 | } 23 | 24 | .post-title-spacer { 25 | flex: 1 1 auto; 26 | } 27 | 28 | .post-footer { 29 | margin: 20px 10px 10px; 30 | font-size: small; 31 | color: gray; 32 | display: flex; 33 | } 34 | 35 | .post-subtitle { 36 | font-size: smaller; 37 | } 38 | 39 | .post-actions { 40 | font-size: xx-small; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /client/src/app/shared/posts-list/posts-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | import {Page, PostHeader, User} from '@app/_models'; 4 | 5 | @Component({selector: 'posts-list', templateUrl: 'posts-list.component.html', styleUrls: ['posts-list.component.scss']}) 6 | export class PostsListComponent { 7 | @Input("posts") posts: Page; 8 | @Input("user") user: User; 9 | @Input("onDelete") onDelete: (id: string) => void = null; 10 | 11 | constructor() { 12 | } 13 | 14 | 15 | delete(id: string) { 16 | if (this.onDelete != null) { 17 | this.onDelete(id); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/app/tags/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tags.component'; 2 | -------------------------------------------------------------------------------- /client/src/app/tags/tags.component.html: -------------------------------------------------------------------------------- 1 |  2 | 3 |

Posts related to #{{tag}}

4 | 5 | 9 | 10 | 11 |
12 | 13 |

There are no Posts on this Blog that match #{{this.tag}}

14 |
15 |
16 | 17 | -------------------------------------------------------------------------------- /client/src/app/tags/tags.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 60%; 3 | min-width: 700px; 4 | justify-content: center; 5 | margin: auto; 6 | display: grid; 7 | } 8 | 9 | h1 { 10 | margin-top: 20px; 11 | } 12 | 13 | 14 | .mat-paginator-container { 15 | margin: auto; 16 | width: 60%; 17 | min-width: 700px; 18 | } 19 | 20 | 21 | .post-card { 22 | margin-bottom: 10px; 23 | min-width: 700px; 24 | } 25 | 26 | .post-cards { 27 | margin-top: 20px; 28 | } 29 | 30 | .post-title { 31 | display: flex; 32 | } 33 | 34 | .post-title-spacer { 35 | flex: 1 1 auto; 36 | } 37 | 38 | .post-footer { 39 | margin: 20px 10px 10px; 40 | font-size: small; 41 | color: gray; 42 | display: flex; 43 | } 44 | 45 | .post-subtitle { 46 | font-size: smaller; 47 | } 48 | 49 | .post-actions { 50 | font-size: xx-small; 51 | } 52 | -------------------------------------------------------------------------------- /client/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eXsio/php-symfony-arch/669ea3ffa016c6baf63b8eaf87ff53de6bc6a62b/client/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiUrl: 'http://localhost:4000' 4 | }; 5 | -------------------------------------------------------------------------------- /client/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | apiUrl: 'http://localhost:4200' 8 | }; 9 | -------------------------------------------------------------------------------- /client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eXsio/php-symfony-arch/669ea3ffa016c6baf63b8eaf87ff53de6bc6a62b/client/src/favicon.ico -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Blog - Symfony Modular, Microservice-ready Architecture Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); -------------------------------------------------------------------------------- /client/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~@angular/material/prebuilt-themes/deeppurple-amber.css"; 3 | 4 | a { cursor: pointer } 5 | html, body { height: 100%; } 6 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 7 | -------------------------------------------------------------------------------- /client/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /client/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ], 19 | "paths": { 20 | "@app/*": ["src/app/*"], 21 | "@environments/*": ["src/environments/*"] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. 3 | It is not intended to be used to perform a compilation. 4 | 5 | To learn more about this file see: https://angular.io/config/solution-tsconfig. 6 | */ 7 | { 8 | "files": [], 9 | "references": [ 10 | { 11 | "path": "./tsconfig.app.json" 12 | }, 13 | { 14 | "path": "./tsconfig.spec.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /client/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.base.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 6 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 7 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 8 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 9 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 10 | Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], 11 | DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], 12 | ]; 13 | -------------------------------------------------------------------------------- /config/jwt/public-test.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoQfAXi56qelrASqyY6G6 3 | RYb+7qvbTBPUNYEjmsZ3m0iLiFOg3VEr29O9ARFdunrEw75Ds2/UOoqTTajD5+nn 4 | Jytu9W0h3e+mbeC2IvkmdQgImnBL9xbGIw9eXZwfxzxndjTNgWaqHQXmhzVAAbSG 5 | fDbCahs8ibQIDM5+EdF3rnPovl6oMoLoz1AMxQA1x8EayCOQ7o1jIGGTuSVbhUMK 6 | oxb7tG3ZpIZouBwhc+8tFPdXOHWCbsbAyEgRXPLww21tHY0hx8ZcoGlq8mcljjx4 7 | sUdfxdSF4RR2CCgF0J6mQ/e5QQVwkv8aPGTZkTaQiF4TqvUMHRokqefkwoW3YaYX 8 | PuSfYn1rzlOTXMtxRRFI77cT7xM7FoAzHqsNcoGc0f1SFAW7PgfYKYKKwjJBiqJ6 9 | 4AUTwPltqgd0MKQQNt7AwxPzcYMaNC7nLoOjA0POc3KLPdvyKXs7KY5y0Xt5xM4k 10 | YkDw6xiksrdjmX1FE5UPlBzEg6Fh9SUdzEblZKycP2QQRIbKCAwS0WirV3Vs5xbN 11 | 2z5Zwuo6/UfbMiXQDJdppoMmlUI0a4yCbxSFz6GjuNjKFW1BhrxURaxaF58cr9/K 12 | pMIM1XT1aRRlddUFifvKXp5utiZHgVzxlq6iPj8r7XKcGytiWmJ/pdrP6zM5uJS3 13 | 1FVK8Rd5dHBUY9HDNSUP/lkCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /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/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | migrations_paths: 3 | # namespace is arbitrary but should be different from App\Migrations 4 | # as migrations classes should NOT be autoloaded 5 | 'DoctrineMigrations': '%kernel.project_dir%/migrations' 6 | enable_profiler: '%kernel.debug%' 7 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | http_method_override: false 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: null 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | storage_factory_id: session.storage.factory.native 14 | 15 | #esi: true 16 | #fragments: true 17 | php_errors: 18 | log: true 19 | 20 | when@test: 21 | framework: 22 | test: true 23 | session: 24 | storage_factory_id: session.storage.factory.mock_file 25 | -------------------------------------------------------------------------------- /config/packages/lexik_jwt_authentication.yaml: -------------------------------------------------------------------------------- 1 | lexik_jwt_authentication: 2 | secret_key: '%env(resolve:JWT_SECRET_KEY)%' 3 | public_key: '%env(resolve:JWT_PUBLIC_KEY)%' 4 | pass_phrase: '%env(JWT_PASSPHRASE)%' 5 | token_ttl: 3600 6 | user_identity_field: email 7 | token_extractors: 8 | cookie: 9 | enabled: true 10 | name: BEARER 11 | -------------------------------------------------------------------------------- /config/packages/messenger.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | messenger: 3 | buses: 4 | event.bus: 5 | default_middleware: allow_no_handlers -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | auto_generate_proxy_classes: false 4 | query_cache_driver: 5 | type: pool 6 | pool: doctrine.system_cache_pool 7 | result_cache_driver: 8 | type: pool 9 | pool: doctrine.result_cache_pool 10 | 11 | framework: 12 | cache: 13 | pools: 14 | doctrine.result_cache_pool: 15 | adapter: cache.app 16 | doctrine.system_cache_pool: 17 | adapter: cache.system 18 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | 5 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 6 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 7 | #default_uri: http://localhost 8 | 9 | when@prod: 10 | framework: 11 | router: 12 | strict_requirements: null 13 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | enable_authenticator_manager: true 3 | # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords 4 | password_hashers: 5 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' 6 | # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider 7 | providers: 8 | database: 9 | entity: 10 | class: App\Modules\Security\Persistence\Doctrine\Entity\User 11 | manager_name: security 12 | property: email 13 | jwt: 14 | lexik_jwt: 15 | class: App\Infrastructure\Security\LoggedInUser 16 | firewalls: 17 | login: 18 | pattern: ^/api/login 19 | provider: database 20 | stateless: true 21 | json_login: 22 | check_path: /api/login_check 23 | success_handler: lexik_jwt_authentication.handler.authentication_success 24 | failure_handler: lexik_jwt_authentication.handler.authentication_failure 25 | 26 | api: 27 | pattern: ^/api/admin 28 | provider: jwt 29 | stateless: true 30 | jwt: ~ 31 | 32 | access_control: 33 | - { path: ^/api/login, roles: PUBLIC_ACCESS } 34 | - { path: ^/api/admin, roles: IS_AUTHENTICATED_FULLY } 35 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | request: 5 | converters: true 6 | auto_convert: true 7 | -------------------------------------------------------------------------------- /config/packages/test/dama_doctrine_test_bundle.yaml: -------------------------------------------------------------------------------- 1 | dama_doctrine_test: 2 | enable_static_connection: true 3 | enable_static_meta_data_cache: true 4 | enable_static_query_cache: true 5 | -------------------------------------------------------------------------------- /config/packages/test/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | # "TEST_TOKEN" is typically set by ParaTest 4 | connections: 5 | posts: 6 | # configure these for your database server 7 | url: '%env(resolve:DATABASE_URL_POSTS_TEST)%' 8 | dbname: posts 9 | tags: 10 | # configure these for your database server 11 | url: '%env(resolve:DATABASE_URL_TAGS_TEST)%' 12 | dbname: tags 13 | comments: 14 | # configure these for your database server 15 | url: '%env(resolve:DATABASE_URL_COMMENTS_TEST)%' 16 | dbname: comments 17 | security: 18 | # configure these for your database server 19 | url: '%env(resolve:DATABASE_URL_SECURITY_TEST)%' 20 | dbname: security -------------------------------------------------------------------------------- /config/packages/test/lexik_jwt_authentication.yaml: -------------------------------------------------------------------------------- 1 | lexik_jwt_authentication: 2 | secret_key: '%kernel.project_dir%/config/jwt/private-test.pem' 3 | public_key: '%kernel.project_dir%/config/jwt/public-test.pem' 4 | pass_phrase: 'test' -------------------------------------------------------------------------------- /config/packages/test/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | not_compromised_password: false 4 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | 5 | # Enables validator auto-mapping support. 6 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 7 | #auto_mapping: 8 | # App\Entity\: [] 9 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | addSql('CREATE TABLE USERS (id BLOB NOT NULL --(DC2Type:ulid) 20 | , email VARCHAR(180) NOT NULL, roles CLOB NOT NULL --(DC2Type:json) 21 | , password VARCHAR(255) NOT NULL, version INTEGER DEFAULT 1 NOT NULL, PRIMARY KEY(id))'); 22 | $this->addSql('CREATE UNIQUE INDEX UNIQ_E3D76759E7927C74 ON USERS (email)'); 23 | } 24 | 25 | public function down(Schema $schema): void 26 | { 27 | $this->addSql('DROP TABLE USERS'); 28 | } 29 | 30 | protected function getDbName(): string 31 | { 32 | return "security"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | entityManager = $this->getManager($managerRegistry); 21 | } 22 | 23 | /** 24 | * @param ManagerRegistry $managerRegistry 25 | * @return EntityManagerInterface 26 | */ 27 | private function getManager(ManagerRegistry $managerRegistry): EntityManagerInterface 28 | { 29 | $name = $this->getManagerName(); 30 | $manager = $managerRegistry->getManager($name); 31 | if ($manager instanceof EntityManagerInterface) { 32 | return $manager; 33 | } 34 | throw new \RuntimeException("No Entity Manager with name '$name'"); 35 | } 36 | 37 | /** 38 | * @return EntityManagerInterface 39 | */ 40 | protected function getEntityManager(): EntityManagerInterface 41 | { 42 | return $this->entityManager; 43 | } 44 | 45 | 46 | abstract protected function getManagerName(): string; 47 | } -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/Migrations/DoctrineMigration.php: -------------------------------------------------------------------------------- 1 | connection->getParams(); 19 | $this->skipIf($parameters['dbname'] != $this->getDbName(), 20 | "This is the other DB\'s migration, pass a correct --em parameter"); 21 | } 22 | 23 | public function preDown(Schema $schema): void 24 | { 25 | $parameters = $this->connection->getParams(); 26 | $this->skipIf($parameters['dbname'] != $this->getDbName(), 27 | "This is the other DB\'s migration, pass a correct --em parameter"); 28 | } 29 | 30 | protected abstract function getDbName(): string; 31 | } -------------------------------------------------------------------------------- /src/Infrastructure/Doctrine/Repository/DoctrineRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager(); 29 | if(!$em->isOpen()) { 30 | $em = $this->managerRegistry->resetManager($this->getManagerName()); 31 | } 32 | return new DoctrineTransaction($em, $func); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Infrastructure/Events/Api/ApplicationEventPublisher.php: -------------------------------------------------------------------------------- 1 | setEventId(EventIdHolder::get()); 27 | } 28 | $this->logger->info(sprintf("[event]: Publishing Outbound Event %s(%s)", $event::class, $event)); 29 | $this->messageBus->dispatch($event); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/Infrastructure/Events/Api/ApplicationEventPublisherInterface.php: -------------------------------------------------------------------------------- 1 | handlerMethodName; 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getInboundEventClass(): string 31 | { 32 | return $this->inboundEventClass; 33 | } 34 | 35 | public static function create(string $handlerMethodName, string $inboundEventClass): EventHandlerReference 36 | { 37 | return new EventHandlerReference($handlerMethodName, $inboundEventClass); 38 | } 39 | 40 | public function getEventName(): string 41 | { 42 | return call_user_func($this->inboundEventClass . '::getName'); 43 | } 44 | 45 | 46 | } -------------------------------------------------------------------------------- /src/Infrastructure/Events/EventIdHolder.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class Page 9 | { 10 | 11 | /** 12 | * @param array $data 13 | * @param int $count 14 | * @param int $pageNo 15 | */ 16 | public function __construct( 17 | private array $data, 18 | private int $count, 19 | private int $pageNo, 20 | private int $pageSize 21 | ) 22 | { 23 | } 24 | 25 | /** 26 | * @return array 27 | */ 28 | public function getData(): array 29 | { 30 | return $this->data; 31 | } 32 | 33 | /** 34 | * @return int 35 | */ 36 | public function getCount(): int 37 | { 38 | return $this->count; 39 | } 40 | 41 | /** 42 | * @return int 43 | */ 44 | public function getPageNo(): int 45 | { 46 | return $this->pageNo; 47 | } 48 | 49 | /** 50 | * @return int 51 | */ 52 | public function getPageSize(): int 53 | { 54 | return $this->pageSize; 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /src/Infrastructure/Security/JWTBaseListener.php: -------------------------------------------------------------------------------- 1 | headers->setCookie( 16 | new Cookie( 17 | "BEARER", 18 | $token, 19 | new \DateTime("+1 day"), 20 | "/", 21 | null, 22 | false, 23 | true, 24 | false, 25 | 'strict' 26 | ) 27 | ); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Infrastructure/Security/JWTLoggedInUserProvider.php: -------------------------------------------------------------------------------- 1 | security->getUser(); 22 | if ($user == null) { 23 | throw new \RuntimeException("You are not logged in!"); 24 | } 25 | if ($user instanceof LoggedInUser) { 26 | return $user; 27 | } 28 | throw new \RuntimeException("Unsupported User Class: " . $user::class); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Infrastructure/Security/LoggedInUserProviderInterface.php: -------------------------------------------------------------------------------- 1 | getConnection()->executeStatement('PRAGMA foreign_keys = ON;'); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Infrastructure/Transactions/TransactionFactoryInterface.php: -------------------------------------------------------------------------------- 1 | postId; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getAuthor(): string 37 | { 38 | return $this->author; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getBody(): string 45 | { 46 | return $this->body; 47 | } 48 | 49 | /** 50 | * @return Ulid|null 51 | */ 52 | public function getParentId(): ?Ulid 53 | { 54 | return $this->parentId; 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Api/Command/Response/CreateCommentCommandResponse.php: -------------------------------------------------------------------------------- 1 | id; 25 | } 26 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Api/Controller/CommentsCommandController.php: -------------------------------------------------------------------------------- 1 | json( 33 | $this->commentsApi->createComment($command) 34 | ); 35 | } 36 | 37 | 38 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Api/Event/Inbound/PostCreatedCommentsIEvent.php: -------------------------------------------------------------------------------- 1 | id = $this->ulid('id'); 25 | $this->title = $this->string('title'); 26 | $this->tags = $this->array('tags'); 27 | } 28 | 29 | /** 30 | * @return Ulid 31 | */ 32 | public function getId(): Ulid 33 | { 34 | return $this->id; 35 | } 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function getTitle(): string 41 | { 42 | return $this->title; 43 | } 44 | 45 | /** 46 | * @return array 47 | */ 48 | public function getTags(): array 49 | { 50 | return $this->tags; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public static function getName(): string 57 | { 58 | return self::EVENT_NAME; 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Api/Event/Inbound/PostDeletedCommentsIEvent.php: -------------------------------------------------------------------------------- 1 | id = $this->ulid('id'); 21 | } 22 | 23 | 24 | /** 25 | * @return Ulid 26 | */ 27 | public function getId(): Ulid 28 | { 29 | return $this->id; 30 | } 31 | 32 | 33 | /** 34 | * @return string 35 | */ 36 | public static function getName(): string 37 | { 38 | return self::EVENT_NAME; 39 | } 40 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Api/Query/FindCommentsByPostIdQuery.php: -------------------------------------------------------------------------------- 1 | postId; 25 | } 26 | 27 | 28 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Api/Query/FindLatestCommentsQuery.php: -------------------------------------------------------------------------------- 1 | pageNo; 23 | } 24 | 25 | 26 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Api/Query/Response/FindCommentsPostHeadersQueryResponse.php: -------------------------------------------------------------------------------- 1 | id; 28 | } 29 | 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getTitle(): string 35 | { 36 | return $this->title; 37 | } 38 | 39 | /** 40 | * @return array 41 | */ 42 | public function getTags(): array 43 | { 44 | return $this->tags; 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function getVersion(): int 51 | { 52 | return $this->version; 53 | } 54 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Dto/CommentsPostHeaderDto.php: -------------------------------------------------------------------------------- 1 | id; 28 | } 29 | 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getTitle(): string 35 | { 36 | return $this->title; 37 | } 38 | 39 | 40 | 41 | /** 42 | * @return array 43 | */ 44 | public function getTags(): array 45 | { 46 | return $this->tags; 47 | } 48 | 49 | /** 50 | * @return int 51 | */ 52 | public function getVersion(): int 53 | { 54 | return $this->version; 55 | } 56 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Dto/CreateNewCommentDto.php: -------------------------------------------------------------------------------- 1 | postId; 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function getAuthor(): string 38 | { 39 | return $this->author; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getBody(): string 46 | { 47 | return $this->body; 48 | } 49 | 50 | /** 51 | * @return Ulid|null 52 | */ 53 | public function getParentId(): ?Ulid 54 | { 55 | return $this->parentId; 56 | } 57 | 58 | /** 59 | * @return \DateTime 60 | */ 61 | public function getCreatedAt(): \DateTime 62 | { 63 | return $this->createdAt; 64 | } 65 | 66 | 67 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Dto/CreateNewCommentsPostHeaderDto.php: -------------------------------------------------------------------------------- 1 | id; 28 | } 29 | 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getTitle(): string 35 | { 36 | return $this->title; 37 | } 38 | 39 | /** 40 | * @return array 41 | */ 42 | public function getTags(): array 43 | { 44 | return $this->tags; 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function getVersion(): int 51 | { 52 | return $this->version; 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Dto/DeleteExistingCommentsPostHeaderDto.php: -------------------------------------------------------------------------------- 1 | id; 22 | } 23 | 24 | 25 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Dto/UpdateExistingCommentsPostHeaderDto.php: -------------------------------------------------------------------------------- 1 | id; 28 | } 29 | 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getTitle(): string 35 | { 36 | return $this->title; 37 | } 38 | 39 | /** 40 | * @return array 41 | */ 42 | public function getTags(): array 43 | { 44 | return $this->tags; 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function getVersion(): int 51 | { 52 | return $this->version; 53 | } 54 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Dto/UpdatedCommentsPostHeadersUserNameDto.php: -------------------------------------------------------------------------------- 1 | oldUserName; 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getNewUserName(): string 31 | { 32 | return $this->newUserName; 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Event/Outbound/CommentCreatedOEvent.php: -------------------------------------------------------------------------------- 1 | $postId, 25 | 'comment' => json_encode($comment) 26 | ] 27 | ); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Event/Outbound/CommentsBaselinedOEvent.php: -------------------------------------------------------------------------------- 1 | $comments 16 | */ 17 | public function __construct( 18 | Ulid $postId, 19 | array $comments 20 | ) 21 | { 22 | parent::__construct(self::EVENT_NAME, 23 | [ 24 | 'postId' => $postId, 25 | 'comments' => json_encode($comments), 26 | ] 27 | ); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Event/Outbound/CommentsCountUpdatedOEvent.php: -------------------------------------------------------------------------------- 1 | $postId, 24 | 'commentsCount' => $commentsCount, 25 | ] 26 | ); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Logic/PostHeadersFinder.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function findPostHeaders(): array 24 | { 25 | return Collection::from($this->headersFindingRepository->findPostHeaders()) 26 | ->map(function ($header) { 27 | return new FindCommentsPostHeadersQueryResponse( 28 | $header->getId(), 29 | $header->getTitle(), 30 | $header->getTags(), 31 | $header->getVersion() 32 | ); 33 | }) 34 | ->toArray(); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Repository/CommentsCreationRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public function findLatestComments(int $pageNo): Page; 31 | 32 | /** 33 | * @param Ulid $commentId 34 | * @return bool 35 | */ 36 | public function commentExists(Ulid $commentId): bool; 37 | 38 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Repository/CommentsPostHeadersFindingRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | public function findPostHeaders(?\DateTime $from = null): array; 14 | 15 | /** 16 | * @param Ulid $postId 17 | * @return bool 18 | */ 19 | public function postExists(Ulid $postId): bool; 20 | 21 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Domain/Repository/CommentsPostsEventsHandlingRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | getEntityManager(); 19 | $expr = $em->getExpressionBuilder(); 20 | $qb = $em->createQueryBuilder(); 21 | 22 | $subQuery = $qb->select('sc.id') 23 | ->from(Comment::class, 'sc') 24 | ->join('sc.post', 'sp') 25 | ->where('sp.id = :postId') 26 | ->getDQL(); 27 | 28 | $qb 29 | ->delete(Comment::class, 'comment') 30 | ->where($expr->in('comment.id', $subQuery)) 31 | ->setParameter('postId', $postId, 'ulid') 32 | ->getQuery() 33 | ->execute(); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Modules/Comments/Persistence/Doctrine/Repository/DoctrineCommentsRepository.php: -------------------------------------------------------------------------------- 1 | $tags 13 | */ 14 | public function __construct( 15 | private string $title, 16 | private string $body, 17 | private array $tags 18 | ) 19 | { 20 | } 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function getTitle(): string 26 | { 27 | return $this->title; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getBody(): string 34 | { 35 | return $this->body; 36 | } 37 | 38 | /** 39 | * @return string[] 40 | */ 41 | public function getTags(): array 42 | { 43 | return $this->tags; 44 | } 45 | 46 | 47 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Api/Command/DeletePostCommand.php: -------------------------------------------------------------------------------- 1 | id; 25 | } 26 | 27 | 28 | public function getResourceName(): string 29 | { 30 | return "post"; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Api/Command/Response/CreatePostCommandResponse.php: -------------------------------------------------------------------------------- 1 | id; 25 | } 26 | 27 | 28 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Api/Event/Inbound/CommentCreatedPostsIEvent.php: -------------------------------------------------------------------------------- 1 | postId = $this->ulid('postId'); 23 | $this->comments = [$this->array('comment')]; 24 | } 25 | 26 | 27 | /** 28 | * @return Ulid|null 29 | */ 30 | public function getPostId(): ?Ulid 31 | { 32 | return $this->postId; 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | public function getComments(): array 39 | { 40 | return $this->comments; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public static function getName(): string 47 | { 48 | return self::EVENT_NAME; 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Api/Event/Inbound/CommentsBaselinedPostsIEvent.php: -------------------------------------------------------------------------------- 1 | postId = $this->ulid('postId'); 23 | $this->comments = $this->array('comments'); 24 | } 25 | 26 | /** 27 | * @return Ulid|null 28 | */ 29 | public function getPostId(): ?Ulid 30 | { 31 | return $this->postId; 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function getComments(): array 38 | { 39 | return $this->comments; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public static function getName(): string 46 | { 47 | return self::EVENT_NAME; 48 | } 49 | 50 | 51 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Api/Event/Inbound/UserRenamedPostsIEvent.php: -------------------------------------------------------------------------------- 1 | oldLogin = $this->string('oldLogin'); 22 | $this->newLogin = $this->string('newLogin'); 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function getOldLogin(): string 29 | { 30 | return $this->oldLogin; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getNewLogin(): string 37 | { 38 | return $this->newLogin; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public static function getName(): string 45 | { 46 | return self::EVENT_NAME; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Api/Query/FindAllPostsQuery.php: -------------------------------------------------------------------------------- 1 | pageNo; 22 | } 23 | 24 | 25 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Api/Query/FindPostByIdQuery.php: -------------------------------------------------------------------------------- 1 | id; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Dto/DeleteExistingPostDto.php: -------------------------------------------------------------------------------- 1 | id; 25 | } 26 | 27 | 28 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Dto/FindExistingPostDto.php: -------------------------------------------------------------------------------- 1 | id; 25 | } 26 | 27 | 28 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Dto/FindExistingPostsDto.php: -------------------------------------------------------------------------------- 1 | pageNo; 23 | } 24 | 25 | 26 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Dto/PostCommentDto.php: -------------------------------------------------------------------------------- 1 | id; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getAuthor(): string 37 | { 38 | return $this->author; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getComment(): string 45 | { 46 | return $this->comment; 47 | } 48 | 49 | /** 50 | * @return \DateTime 51 | */ 52 | public function getCreatedAt(): \DateTime 53 | { 54 | return $this->createdAt; 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Dto/UpdatePostCommentsDto.php: -------------------------------------------------------------------------------- 1 | postId; 27 | } 28 | 29 | /** 30 | * @return array 31 | */ 32 | public function getComments(): array 33 | { 34 | return $this->comments; 35 | } 36 | 37 | 38 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Dto/UpdatedPostsUserNameDto.php: -------------------------------------------------------------------------------- 1 | oldUserName; 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getNewUserName(): string 31 | { 32 | return $this->newUserName; 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Event/Outbound/PostBaselinedOEvent.php: -------------------------------------------------------------------------------- 1 | $baselinedPost->getId(), 22 | 'title' => $baselinedPost->getTitle(), 23 | 'summary' => $baselinedPost->getSummary(), 24 | 'body' => $baselinedPost->getBody(), 25 | 'tags' => $baselinedPost->getTags(), 26 | 'createdById' => $baselinedPost->getCreatedById(), 27 | 'createdByName' => $baselinedPost->getCreatedByName(), 28 | 'createdAt' => $baselinedPost->getCreatedAt(), 29 | 'updatedAt' => $baselinedPost->getUpdatedAt(), 30 | 'version' => $baselinedPost->getVersion() 31 | ] 32 | ); 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Event/Outbound/PostCreatedOEvent.php: -------------------------------------------------------------------------------- 1 | $id, 25 | 'title' => $newPost->getTitle(), 26 | 'summary' => $newPost->getSummary(), 27 | 'body' => $newPost->getBody(), 28 | 'tags' => $newPost->getTags(), 29 | 'createdById' => $newPost->getCreatedById(), 30 | 'createdByName' => $newPost->getCreatedByName(), 31 | 'createdAt' => $newPost->getCreatedAt() 32 | ] 33 | ); 34 | } 35 | 36 | 37 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Event/Outbound/PostDeletedOEvent.php: -------------------------------------------------------------------------------- 1 | $deletedPost->getId()]); 20 | } 21 | 22 | 23 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Event/Outbound/PostUpdatedOEvent.php: -------------------------------------------------------------------------------- 1 | $updatedPost->getId(), 23 | 'body' => $updatedPost->getBody(), 24 | 'tags' => $updatedPost->getTags(), 25 | 'title' => $updatedPost->getTitle(), 26 | 'summary' => $updatedPost->getSummary(), 27 | 'updatedAt' => $updatedPost->getUpdatedAt(), 28 | 'lastVersion' => $lastVersion 29 | ] 30 | ); 31 | } 32 | 33 | 34 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Logic/SecurityEventsHandler.php: -------------------------------------------------------------------------------- 1 | transactionFactory 26 | ->createTransaction(function () use ($event) { 27 | $this->securityEventsHandlingRepository->updateUserName( 28 | new UpdatedPostsUserNameDto( 29 | $event->getOldLogin(), 30 | $event->getNewLogin() 31 | ) 32 | ); 33 | }) 34 | ->execute(); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Repository/PostsCommentsEventHandlingRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public function findPosts(int $pageNo): Page; 26 | 27 | /** 28 | * @param \DateTime|null $from 29 | * @return array 30 | */ 31 | public function findExistingPostsForBaseline(?\DateTime $from): array; 32 | 33 | /** 34 | * @param \DateTime|null $from 35 | * @return array 36 | */ 37 | public function findDeletedPostIdsForBaseline(?\DateTime $from): array; 38 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Domain/Repository/PostsSecurityEventsHandlingRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | getEntityManager() 15 | ->getRepository(PostComments::class) 16 | ->findOneBy(["postId" => $updatedComments->getPostId()]); 17 | if($append) { 18 | $data = $comments->getComments(); 19 | foreach ($updatedComments->getComments() as $newComment) { 20 | array_push($data, $newComment); 21 | } 22 | $comments->setComments($data); 23 | } else { 24 | $comments->setComments($updatedComments->getComments()); 25 | } 26 | 27 | $comments->setCommentsCount(count($comments->getComments())); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Persistence/Doctrine/Repository/DoctrinePostsDeletionRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager(); 19 | $em 20 | ->createQueryBuilder() 21 | ->delete(PostComments::class, 'pc') 22 | ->where('pc.postId = :id') 23 | ->setParameter('id', $dto->getId(), 'ulid') 24 | ->getQuery() 25 | ->execute(); 26 | 27 | $em 28 | ->createQueryBuilder() 29 | ->update(Post::class, 'p') 30 | ->set("p.deletedAt", ":deletedAt") 31 | ->where('p.id = :id') 32 | ->setParameter('id', $dto->getId(), 'ulid') 33 | ->setParameter('deletedAt', new \DateTime()) 34 | ->getQuery() 35 | ->execute(); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Persistence/Doctrine/Repository/DoctrinePostsRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager() 15 | ->createQueryBuilder() 16 | ->update(Post::class, 'p') 17 | ->set('p.createdByName', ':newUserName') 18 | ->where('p.createdByName = :oldUserName') 19 | ->getQuery() 20 | ->setParameter('newUserName', $updatedUserName->getNewUserName()) 21 | ->setParameter('oldUserName', $updatedUserName->getOldUserName()) 22 | ->execute(); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Persistence/Doctrine/Repository/DoctrinePostsUpdatingRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager() 19 | ->createQueryBuilder() 20 | ->update(Post::class, 'p') 21 | ->set('p.title', ':title') 22 | ->set('p.summary', ':summary') 23 | ->set('p.body', ':body') 24 | ->set('p.tags', ':tags') 25 | ->where('p.id = :id') 26 | ->setParameter("id", $dto->getId(), 'ulid') 27 | ->setParameter("title", $dto->getTitle()) 28 | ->setParameter("summary", $dto->getSummary()) 29 | ->setParameter("body", $dto->getBody()) 30 | ->setParameter("tags", json_encode($dto->getTags())) 31 | ->getQuery() 32 | ->execute(); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Modules/Posts/Persistence/Doctrine/Transactions/DoctrinePostTransactionFactory.php: -------------------------------------------------------------------------------- 1 | id; 25 | } 26 | 27 | 28 | } -------------------------------------------------------------------------------- /src/Modules/Security/Api/Controller/LogoutController.php: -------------------------------------------------------------------------------- 1 | headers->clearCookie('BEARER'); 17 | return $response; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Modules/Security/Api/SecurityApiInterface.php: -------------------------------------------------------------------------------- 1 | login; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getPassword(): string 27 | { 28 | return $this->password; 29 | } 30 | 31 | 32 | } -------------------------------------------------------------------------------- /src/Modules/Security/Domain/Dto/CreateNewUserDto.php: -------------------------------------------------------------------------------- 1 | login; 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | public function getEncryptedPassword(): string 33 | { 34 | return $this->encryptedPassword; 35 | } 36 | 37 | /** 38 | * @return array 39 | */ 40 | public function getRoles(): array 41 | { 42 | return $this->roles; 43 | } 44 | 45 | 46 | } -------------------------------------------------------------------------------- /src/Modules/Security/Domain/Dto/DeleteExistingUserPostHeaderDto.php: -------------------------------------------------------------------------------- 1 | id; 22 | } 23 | 24 | 25 | } -------------------------------------------------------------------------------- /src/Modules/Security/Domain/Dto/RenameExistingUserDto.php: -------------------------------------------------------------------------------- 1 | currentLogin; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getNewLogin(): string 27 | { 28 | return $this->newLogin; 29 | } 30 | 31 | 32 | } -------------------------------------------------------------------------------- /src/Modules/Security/Domain/Dto/UpdatePostsCommentsCountDto.php: -------------------------------------------------------------------------------- 1 | postId; 26 | } 27 | 28 | /** 29 | * @return int 30 | */ 31 | public function getCommentsCount(): int 32 | { 33 | return $this->commentsCount; 34 | } 35 | 36 | 37 | } -------------------------------------------------------------------------------- /src/Modules/Security/Domain/Event/Outbound/UserRenamedOEvent.php: -------------------------------------------------------------------------------- 1 | $oldLogin, 23 | 'newLogin' => $newLogin 24 | ] 25 | ); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Modules/Security/Domain/Logic/PasswordHasher.php: -------------------------------------------------------------------------------- 1 | passwordHasher->hashPassword(new class implements PasswordAuthenticatedUserInterface { 20 | public function getPassword(): ?string 21 | { 22 | return null; 23 | } 24 | }, $plainPassword); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Modules/Security/Domain/Provider/SymfonyDatabaseLoggedInUserProvider.php: -------------------------------------------------------------------------------- 1 | security->getUser(); 28 | if ($user == null) { 29 | throw new \RuntimeException("You are not logged in!"); 30 | } 31 | if ($user instanceof LoggedInUser) { 32 | return $user; 33 | } 34 | if ($user instanceof User) { 35 | return new LoggedInUser($user->getId(), $user->getUserIdentifier(), $user->getRoles()); 36 | } 37 | throw new \RuntimeException("Unsupported User Class: " . $user::class); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Modules/Security/Domain/Repository/UserCreationRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | setEmail($newUser->getLogin()); 20 | $user->setPassword($newUser->getEncryptedPassword()); 21 | $user->setRoles($newUser->getRoles()); 22 | $this->getEntityManager()->persist($user); 23 | return $user->getId(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Modules/Security/Persistence/Doctrine/Repository/DoctrineUserFindingRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager() 19 | ->createQuery("select count(u.id) as count from $entityClass u 20 | where u.email = :login") 21 | ->setParameter("login", $login) 22 | ->getResult()[0]["count"] > 0; 23 | } 24 | } -------------------------------------------------------------------------------- /src/Modules/Security/Persistence/Doctrine/Transactions/DoctrineSecurityTransactionFactory.php: -------------------------------------------------------------------------------- 1 | json( 32 | $this->tagsApi->findTags() 33 | ); 34 | } 35 | 36 | /** 37 | * @param string $tag 38 | * @return Response 39 | */ 40 | #[Route("/{tag}", methods: ['GET'])] 41 | public function findPostsByTag(string $tag, Request $request): Response 42 | { 43 | $pageNo = $request->query->get("pageNo", "1"); 44 | return $this->json( 45 | $this->tagsApi->findPostsByTag(new FindPostsByTagQuery($tag, intval($pageNo))) 46 | ); 47 | } 48 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Api/Event/Inbound/CommentsCountUpdatedTagsIEvent.php: -------------------------------------------------------------------------------- 1 | postId = $this->ulid('postId'); 23 | $this->commentsCount = $this->int('commentsCount'); 24 | } 25 | 26 | /** 27 | * @return Ulid|null 28 | */ 29 | public function getPostId(): ?Ulid 30 | { 31 | return $this->postId; 32 | } 33 | 34 | /** 35 | * @return int|null 36 | */ 37 | public function getCommentsCount(): ?int 38 | { 39 | return $this->commentsCount; 40 | } 41 | 42 | 43 | /** 44 | * @return string 45 | */ 46 | public static function getName(): string 47 | { 48 | return self::EVENT_NAME; 49 | } 50 | 51 | 52 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Api/Event/Inbound/PostDeletedTagsIEvent.php: -------------------------------------------------------------------------------- 1 | id = $this->ulid('id'); 21 | } 22 | 23 | 24 | /** 25 | * @return Ulid 26 | */ 27 | public function getId(): Ulid 28 | { 29 | return $this->id; 30 | } 31 | 32 | 33 | /** 34 | * @return string 35 | */ 36 | public static function getName(): string 37 | { 38 | return self::EVENT_NAME; 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Api/Event/Inbound/UserRenamedTagsIEvent.php: -------------------------------------------------------------------------------- 1 | oldLogin = $this->string('oldLogin'); 22 | $this->newLogin = $this->string('newLogin'); 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function getOldLogin(): string 29 | { 30 | return $this->oldLogin; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getNewLogin(): string 37 | { 38 | return $this->newLogin; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public static function getName(): string 45 | { 46 | return self::EVENT_NAME; 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Api/Query/FindPostsByTagQuery.php: -------------------------------------------------------------------------------- 1 | tag; 24 | } 25 | 26 | /** 27 | * @return int 28 | */ 29 | public function getPageNo(): int 30 | { 31 | return $this->pageNo; 32 | } 33 | 34 | 35 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Api/Query/Response/FindTagsQueryResponse.php: -------------------------------------------------------------------------------- 1 | tag; 24 | } 25 | 26 | /** 27 | * @return int 28 | */ 29 | public function getPostsCount(): int 30 | { 31 | return $this->postsCount; 32 | } 33 | 34 | 35 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Domain/Dto/DeleteExistingTagsPostHeaderDto.php: -------------------------------------------------------------------------------- 1 | id; 22 | } 23 | 24 | 25 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Domain/Dto/TagDto.php: -------------------------------------------------------------------------------- 1 | tag; 24 | } 25 | 26 | /** 27 | * @return int 28 | */ 29 | public function getPostsCount(): int 30 | { 31 | return $this->postsCount; 32 | } 33 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Domain/Dto/UpdatePostsCommentsCountDto.php: -------------------------------------------------------------------------------- 1 | postId; 26 | } 27 | 28 | /** 29 | * @return int 30 | */ 31 | public function getCommentsCount(): int 32 | { 33 | return $this->commentsCount; 34 | } 35 | 36 | 37 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Domain/Dto/UpdatedTagsPostHeadersUserNameDto.php: -------------------------------------------------------------------------------- 1 | oldUserName; 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getNewUserName(): string 31 | { 32 | return $this->newUserName; 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Domain/Logic/SecurityEventsHandler.php: -------------------------------------------------------------------------------- 1 | transactionFactory 26 | ->createTransaction(function () use ($event) { 27 | $this->securityEventsHandlingRepository->updateUserName( 28 | new UpdatedTagsPostHeadersUserNameDto( 29 | $event->getOldLogin(), 30 | $event->getNewLogin() 31 | ) 32 | ); 33 | }) 34 | ->execute(); 35 | } 36 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Domain/Logic/TagsFinder.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function findTags(): array 24 | { 25 | return 26 | Collection::from($this->tagsFindingRepository->findTags()) 27 | ->map(function ($tag) { 28 | return new FindTagsQueryResponse($tag->getTag(), $tag->getPostsCount()); 29 | }) 30 | ->toArray(); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Domain/Logic/TagsUpdater.php: -------------------------------------------------------------------------------- 1 | tagsUpdatingRepository->updatePostTags($postId, $tags); 30 | $this->deleteEmptyTags(); 31 | } 32 | 33 | 34 | public function deleteEmptyTags(): void 35 | { 36 | $this->tagsDeletingRepository->deleteEmptyTags(); 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Domain/Repository/TagsCommentsEventHandlingRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | public function findTags(): array; 14 | 15 | 16 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Domain/Repository/TagsPostEventsHandlingRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function findPostHeaders(): array; 17 | 18 | /** 19 | * @param string $tag 20 | * @param int $pageNo 21 | * @return Page 22 | */ 23 | public function findPostHeadersByTag(string $tag, int $pageNo): Page; 24 | 25 | /** 26 | * @param Ulid $postId 27 | * @return bool 28 | */ 29 | public function postExists(Ulid $postId): bool; 30 | 31 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Domain/Repository/TagsSecurityEventsHandlingRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | tag; 28 | } 29 | 30 | /** 31 | * @param Tag $tag 32 | */ 33 | public function setTag(Tag $tag): void 34 | { 35 | $this->tag = $tag; 36 | } 37 | 38 | /** 39 | * @return TagPostHeader 40 | */ 41 | public function getPost(): TagPostHeader 42 | { 43 | return $this->post; 44 | } 45 | 46 | /** 47 | * @param TagPostHeader $post 48 | */ 49 | public function setPost(TagPostHeader $post): void 50 | { 51 | $this->post = $post; 52 | } 53 | 54 | 55 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Persistence/Doctrine/Repository/DoctrineTagsCommentsEventHandlingRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager() 18 | ->createQueryBuilder() 19 | ->update(TagPostHeader::class, 'p') 20 | ->set('p.commentsCount', ':count') 21 | ->where('p.id = :id') 22 | ->getQuery() 23 | ->setParameter(':count', $commentsCount->getCommentsCount()) 24 | ->setParameter(':id', $commentsCount->getPostId(), 'ulid') 25 | ->execute(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Persistence/Doctrine/Repository/DoctrineTagsDeletingRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager(); 15 | $expr = $em->getExpressionBuilder(); 16 | $emptyTags = $em->createQueryBuilder() 17 | ->select('tag.id') 18 | ->from(Tag::class, 'tag') 19 | ->where( 20 | $expr->notIn('tag.id', 21 | $em->createQueryBuilder() 22 | ->select('subTag.id') 23 | ->from(TagPost::class, 'subTp') 24 | ->innerJoin('subTp.tag', 'subTag') 25 | ->getDQL() 26 | ) 27 | ) 28 | ->getQuery() 29 | ->getDQL(); 30 | 31 | $this->getEntityManager() 32 | ->createQueryBuilder() 33 | ->delete(Tag::class, 't') 34 | ->where($expr->in('t.id', $emptyTags)) 35 | ->getQuery() 36 | ->execute(); 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Persistence/Doctrine/Repository/DoctrineTagsFindingRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager() 20 | ->createQuery("select new $dtoClass(tag.tag, count(post.id)) 21 | from $tagClass tag 22 | join tag.tagPosts tp 23 | join tp.post post 24 | group by tag.tag 25 | order by count(post.id) desc") 26 | ->getArrayResult(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Persistence/Doctrine/Repository/DoctrineTagsRepository.php: -------------------------------------------------------------------------------- 1 | getEntityManager() 16 | ->createQueryBuilder() 17 | ->update(TagPostHeader::class, 'p') 18 | ->set('p.createdByName', ':newUserName') 19 | ->where('p.createdByName = :oldUserName') 20 | ->getQuery() 21 | ->setParameter('newUserName', $updatedUserName->getNewUserName()) 22 | ->setParameter('oldUserName', $updatedUserName->getOldUserName()) 23 | ->execute(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Modules/Tags/Persistence/Doctrine/Transactions/DoctrineTagsTransactionFactory.php: -------------------------------------------------------------------------------- 1 | 'handleEvent', 18 | TestApplicationInboundErrorEvent::class => 'handleEventError', 19 | ]; 20 | } 21 | 22 | public function handleEvent(TestApplicationInboundEvent $event) 23 | { 24 | $this->event = $event; 25 | } 26 | 27 | public function handleEventError() 28 | { 29 | throw new \RuntimeException("error"); 30 | } 31 | 32 | /** 33 | * @return TestApplicationInboundEvent|null 34 | */ 35 | public function getEvent(): ?TestApplicationInboundEvent 36 | { 37 | return $this->event; 38 | } 39 | 40 | 41 | } -------------------------------------------------------------------------------- /tests/Infrastructure/Events/Api/TestMessageBus.php: -------------------------------------------------------------------------------- 1 | subscriber->subscribe(); 21 | // $methodName = $events['TEST_EVENT']->getHandlerMethodName(); 22 | // $inboundEventCclass = $events['TEST_EVENT']->getInboundEventClass(); 23 | $this->subscriber->__invoke($message); 24 | return new Envelope($message, $stamps); 25 | } 26 | } -------------------------------------------------------------------------------- /tests/Infrastructure/Events/EventIdHolderSpec.php: -------------------------------------------------------------------------------- 1 | verifyContract(PostBaselinedCommentsIEvent::class, "Comments/PostBaselinedCommentsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Comments/Contracts/Inbound/PostCreatedIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(PostCreatedCommentsIEvent::class, "Comments/PostCreatedCommentsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Comments/Contracts/Inbound/PostDeletedIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(PostDeletedCommentsIEvent::class, "Comments/PostDeletedCommentsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Comments/Contracts/Inbound/PostUpdatedIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(PostUpdatedCommentsIEvent::class, "Comments/PostUpdatedCommentsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Comments/Contracts/Outbound/CommentCreatedOEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContracts($this->createEvent(), ['Posts/CommentCreatedPostsIEvent']) 17 | ); 18 | } 19 | 20 | /** 21 | * @return CommentCreatedOEvent 22 | */ 23 | protected function createEvent(): CommentCreatedOEvent 24 | { 25 | return new CommentCreatedOEvent( 26 | new Ulid(), 27 | new CommentDto( 28 | new Ulid(), 29 | 'Post Body', 30 | 'Post Body', 31 | new Ulid(), 32 | new \DateTime() 33 | )); 34 | } 35 | } -------------------------------------------------------------------------------- /tests/Modules/Comments/Contracts/Outbound/CommentsBaselinedOEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContracts($this->createEvent(), ['Posts/CommentsBaselinedPostsIEvent']) 17 | ); 18 | } 19 | 20 | /** 21 | * @return CommentsBaselinedOEvent 22 | */ 23 | protected function createEvent(): CommentsBaselinedOEvent 24 | { 25 | return new CommentsBaselinedOEvent( 26 | new Ulid(), 27 | [new CommentDto(new Ulid(), '', '', null, new \DateTime())]); 28 | } 29 | } -------------------------------------------------------------------------------- /tests/Modules/Comments/Contracts/Outbound/CommentsCountUpdatedOEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContracts($this->createEvent(), [ 16 | 'Tags/CommentsCountUpdatedTagsIEvent', 17 | ]) 18 | ); 19 | } 20 | 21 | /** 22 | * @return CommentsCountUpdatedOEvent 23 | */ 24 | protected function createEvent(): CommentsCountUpdatedOEvent 25 | { 26 | return new CommentsCountUpdatedOEvent( 27 | new Ulid(), 28 | 3); 29 | } 30 | } -------------------------------------------------------------------------------- /tests/Modules/Comments/Unit/Transaction/InMemoryCommentsTransactionFactory.php: -------------------------------------------------------------------------------- 1 | verifyContract(CommentCreatedPostsIEvent::class, "Posts/CommentCreatedPostsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Posts/Contracts/Inbound/CommentsBaselinedPostsIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(CommentsBaselinedPostsIEvent::class, "Posts/CommentsBaselinedPostsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Posts/Contracts/Inbound/UserRenamedPostsIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(UserRenamedPostsIEvent::class, "Posts/UserRenamedPostsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Posts/Contracts/Outbound/PostBaselinedOEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContracts($this->createEvent(), [ 17 | 'Comments/PostBaselinedCommentsIEvent', 18 | 'Tags/PostBaselinedTagsIEvent', 19 | ]) 20 | ); 21 | } 22 | 23 | /** 24 | * @return PostBaselinedOEvent 25 | */ 26 | protected function createEvent(): PostBaselinedOEvent 27 | { 28 | return new PostBaselinedOEvent( 29 | new PostForBaselineDto( 30 | 'Post Title', 31 | 'Post Body', 32 | 'Post Body', 33 | 'Post Body', 34 | ['t1', 't2'], 35 | new Ulid(), 36 | 'userId', 37 | new \DateTime(), 38 | new \DateTime(), 39 | 1 40 | )); 41 | } 42 | } -------------------------------------------------------------------------------- /tests/Modules/Posts/Contracts/Outbound/PostCreatedOEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContracts($this->createEvent(), [ 17 | 'Comments/PostCreatedCommentsIEvent', 18 | 'Tags/PostCreatedTagsIEvent', 19 | ]) 20 | ); 21 | } 22 | 23 | /** 24 | * @return PostCreatedOEvent 25 | */ 26 | protected function createEvent(): PostCreatedOEvent 27 | { 28 | return new PostCreatedOEvent( 29 | new Ulid(), 30 | new CreateNewPostDto( 31 | 'Post Title', 32 | 'Post Body', 33 | 'Post Body', 34 | ['t1', 't2'], 35 | new Ulid(), 36 | 'userId', 37 | new \DateTime() 38 | )); 39 | } 40 | } -------------------------------------------------------------------------------- /tests/Modules/Posts/Contracts/Outbound/PostDeletedOEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContracts($this->createEvent(), [ 17 | 'Comments/PostDeletedCommentsIEvent', 18 | 'Tags/PostDeletedTagsIEvent', 19 | ]) 20 | ); 21 | } 22 | 23 | /** 24 | * @return PostDeletedOEvent 25 | */ 26 | protected function createEvent(): PostDeletedOEvent 27 | { 28 | return new PostDeletedOEvent(new DeleteExistingPostDto(new Ulid())); 29 | } 30 | } -------------------------------------------------------------------------------- /tests/Modules/Posts/Contracts/Outbound/PostUpdatedOEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContracts($this->createEvent(), [ 17 | 'Comments/PostUpdatedCommentsIEvent', 18 | 'Tags/PostUpdatedTagsIEvent', 19 | ]) 20 | ); 21 | } 22 | 23 | /** 24 | * @return PostUpdatedOEvent 25 | */ 26 | protected function createEvent(): PostUpdatedOEvent 27 | { 28 | return new PostUpdatedOEvent( 29 | new UpdateExistingPostDto( 30 | new Ulid(), 31 | 'Post Title', 32 | 'Post Body', 33 | 'Post Body', 34 | ['t1', 't2'], 35 | new \DateTime() 36 | ), 37 | 1); 38 | } 39 | } -------------------------------------------------------------------------------- /tests/Modules/Posts/Unit/PostsSpec.php: -------------------------------------------------------------------------------- 1 | postsApi = new PostsApi( 25 | new InMemoryEventPublisher(), 26 | $repository, 27 | $repository, 28 | $repository, 29 | $repository, 30 | new InMemoryLoggedInUserProvider(), 31 | new InMemoryPostTransactionFactory(), 32 | $repository, 33 | $repository, 34 | $this->createMock(LoggerInterface::class) 35 | ); 36 | } 37 | } -------------------------------------------------------------------------------- /tests/Modules/Posts/Unit/Transaction/InMemoryPostTransactionFactory.php: -------------------------------------------------------------------------------- 1 | verifyContracts($this->createEvent(), [ 15 | 'Posts/UserRenamedPostsIEvent', 16 | 'Tags/UserRenamedTagsIEvent', 17 | ]) 18 | ); 19 | } 20 | 21 | /** 22 | * @return UserRenamedOEvent 23 | */ 24 | protected function createEvent(): UserRenamedOEvent 25 | { 26 | return new UserRenamedOEvent( 27 | "oldLogin@exsio.com", 28 | "newLogin@exsio.com" 29 | ); 30 | } 31 | } -------------------------------------------------------------------------------- /tests/Modules/Security/Integration/Http/SecurityHttpTrait.php: -------------------------------------------------------------------------------- 1 | getClient(); 16 | $client->request('POST', '/api/logout/'); 17 | return $client->getResponse()->getContent(); 18 | } 19 | 20 | /** 21 | * @return KernelBrowser 22 | */ 23 | public abstract function getClient(): KernelBrowser; 24 | } -------------------------------------------------------------------------------- /tests/Modules/Tags/Contracts/Inbound/CommentsCountUpdatedTagsIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(CommentsCountUpdatedTagsIEvent::class, "Tags/CommentsCountUpdatedTagsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Tags/Contracts/Inbound/PostBaselinedIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(PostCreatedTagsIEvent::class, "Tags/PostBaselinedTagsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Tags/Contracts/Inbound/PostCreatedIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(PostCreatedTagsIEvent::class, "Tags/PostCreatedTagsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Tags/Contracts/Inbound/PostDeletedIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(PostDeletedTagsIEvent::class, "Tags/PostDeletedTagsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Tags/Contracts/Inbound/PostUpdatedIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(PostUpdatedTagsIEvent::class, "Tags/PostUpdatedTagsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Tags/Contracts/Inbound/UserRenamedTagsIEventContract.php: -------------------------------------------------------------------------------- 1 | verifyContract(UserRenamedTagsIEvent::class, "Tags/UserRenamedTagsIEvent") 15 | ); 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /tests/Modules/Tags/Integration/Http/TagsHttpTrait.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function findTags(): array 21 | { 22 | $client = $this->getClient(); 23 | $client->request('GET', '/api/tags/'); 24 | return $this->responseObjects($client, FindTagsQueryResponse::class); 25 | } 26 | 27 | /** 28 | * @param FindPostsByTagQuery $query 29 | * @return Page 30 | */ 31 | public function findPostByTag(FindPostsByTagQuery $query): Page 32 | { 33 | $client = $this->getClient(); 34 | $client->request('GET', '/api/tags/' . $query->getTag()); 35 | return $this->responseObject($client, Page::class); 36 | } 37 | 38 | /** 39 | * @return KernelBrowser 40 | */ 41 | public abstract function getClient(): KernelBrowser; 42 | } -------------------------------------------------------------------------------- /tests/Modules/Tags/Unit/Repository/InMemoryTag.php: -------------------------------------------------------------------------------- 1 | id; 25 | } 26 | 27 | 28 | /** 29 | * @return string 30 | */ 31 | public function getTag(): string 32 | { 33 | return $this->tag; 34 | } 35 | 36 | /** 37 | * @param string $tag 38 | */ 39 | public function setTag(string $tag): void 40 | { 41 | $this->tag = $tag; 42 | } 43 | 44 | 45 | 46 | 47 | } -------------------------------------------------------------------------------- /tests/Modules/Tags/Unit/TagsSpec.php: -------------------------------------------------------------------------------- 1 | tagsApi = new TagsApi( 23 | new InMemoryTagsTransactionFactory(), 24 | $repository, 25 | $repository, 26 | $this->createMock(LoggerInterface::class), 27 | $repository, 28 | $repository, 29 | $repository, 30 | $repository, 31 | $repository, 32 | ); 33 | } 34 | } -------------------------------------------------------------------------------- /tests/Modules/Tags/Unit/Transaction/InMemoryTagsTransactionFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | protected function readContract(string $contractName): array 14 | { 15 | $contractPath = sprintf("%s/%s.json", $this->getContractsPath(), $contractName); 16 | if (!file_exists($contractPath)) { 17 | $this->fail(sprintf("Contract File doesn't exist: %s", $contractPath)); 18 | } 19 | 20 | $string = file_get_contents($contractPath); 21 | $data = json_decode($string, true); 22 | return $data; 23 | } 24 | 25 | protected function getInboundEvent(string $contract): array 26 | { 27 | $data = $this->readContract($contract); 28 | $data['_eventId'] = new Ulid(); 29 | return $data; 30 | } 31 | 32 | protected function getContractsPath(): string 33 | { 34 | return dirname(__FILE__) . '/../../../contracts'; 35 | } 36 | 37 | public static abstract function fail(string $message); 38 | } -------------------------------------------------------------------------------- /tests/TestUtils/Contracts/ApplicationInboundEventContract.php: -------------------------------------------------------------------------------- 1 | getInboundEvent($contract); 15 | 16 | try { 17 | new $eventClass($data); 18 | } catch (\Exception $e) { 19 | $this->fail(sprintf("Event Class %s cannot be constructed with the Contract Data '%s': %s", $eventClass, $contract, $e->getMessage())); 20 | } 21 | return true; 22 | } 23 | 24 | 25 | } -------------------------------------------------------------------------------- /tests/TestUtils/Security/InMemoryLoggedInUserProvider.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | } 12 | --------------------------------------------------------------------------------