├── .env ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── labeler.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── apps ├── backoffice │ ├── backend │ │ ├── bin │ │ │ └── console │ │ ├── config │ │ │ ├── bundles.php │ │ │ ├── routes │ │ │ │ ├── courses.yaml │ │ │ │ ├── health-check.yaml │ │ │ │ └── metrics.yaml │ │ │ ├── services.yaml │ │ │ ├── services │ │ │ │ └── framework.yaml │ │ │ └── services_test.yaml │ │ ├── public │ │ │ └── index.php │ │ ├── src │ │ │ ├── BackofficeBackendKernel.php │ │ │ └── Controller │ │ │ │ ├── Courses │ │ │ │ └── CoursesGetController.php │ │ │ │ ├── HealthCheck │ │ │ │ └── HealthCheckGetController.php │ │ │ │ └── Metrics │ │ │ │ └── MetricsController.php │ │ └── var │ │ │ └── .gitkeep │ └── frontend │ │ ├── bin │ │ └── console │ │ ├── config │ │ ├── bundles.php │ │ ├── routes │ │ │ ├── api_courses.yaml │ │ │ ├── courses.yaml │ │ │ ├── health-check.yaml │ │ │ ├── home.yaml │ │ │ └── metrics.yaml │ │ ├── services.yaml │ │ ├── services │ │ │ └── framework.yaml │ │ └── services_test.yaml │ │ ├── public │ │ ├── images │ │ │ └── logo.png │ │ └── index.php │ │ ├── src │ │ ├── BackofficeFrontendKernel.php │ │ ├── Command │ │ │ └── ImportCoursesToElasticsearchCommand.php │ │ └── Controller │ │ │ ├── Courses │ │ │ ├── CoursesGetWebController.php │ │ │ └── CoursesPostWebController.php │ │ │ ├── HealthCheck │ │ │ └── HealthCheckGetController.php │ │ │ ├── Home │ │ │ └── HomeGetWebController.php │ │ │ └── Metrics │ │ │ └── MetricsController.php │ │ ├── templates │ │ ├── master.html.twig │ │ ├── pages │ │ │ ├── courses │ │ │ │ ├── courses.html.twig │ │ │ │ └── partials │ │ │ │ │ ├── list_courses.html.twig │ │ │ │ │ └── new_course_form.html.twig │ │ │ └── home.html.twig │ │ └── partials │ │ │ ├── footer.html.twig │ │ │ └── header.html.twig │ │ └── var │ │ └── .gitkeep ├── bootstrap.php └── mooc │ ├── backend │ ├── bin │ │ └── console │ ├── config │ │ ├── bundles.php │ │ ├── routes │ │ │ ├── courses.yaml │ │ │ ├── courses_counter.yaml │ │ │ ├── health-check.yaml │ │ │ └── metrics.yaml │ │ ├── services.yaml │ │ ├── services │ │ │ └── framework.yaml │ │ └── services_test.yaml │ ├── public │ │ └── index.php │ ├── src │ │ ├── Command │ │ │ └── DomainEvents │ │ │ │ ├── MySql │ │ │ │ └── ConsumeMySqlDomainEventsCommand.php │ │ │ │ ├── PublishDomainEventsFromMutationsCommand.php │ │ │ │ └── RabbitMq │ │ │ │ ├── ConfigureRabbitMqCommand.php │ │ │ │ ├── ConsumeRabbitMqDomainEventsCommand.php │ │ │ │ └── GenerateSupervisorRabbitMqConsumerFilesCommand.php │ │ ├── Controller │ │ │ ├── Courses │ │ │ │ └── CoursesPutController.php │ │ │ ├── CoursesCounter │ │ │ │ └── CoursesCounterGetController.php │ │ │ ├── HealthCheck │ │ │ │ └── HealthCheckGetController.php │ │ │ └── Metrics │ │ │ │ └── MetricsController.php │ │ └── MoocBackendKernel.php │ ├── tests │ │ ├── features │ │ │ ├── courses │ │ │ │ └── course_put.feature │ │ │ ├── courses_counter │ │ │ │ └── courses_counter_get.feature │ │ │ └── health_check │ │ │ │ └── health_check_get.feature │ │ └── mooc_backend.yml │ └── var │ │ └── .gitkeep │ └── frontend │ ├── src │ └── .gitkeep │ └── var │ └── .gitkeep ├── behat.yml ├── composer.json ├── composer.lock ├── docker-compose.yml ├── ecs.php ├── etc ├── databases │ ├── backoffice │ │ └── courses.json │ └── mooc.sql ├── endpoints │ ├── backoffice_frontend.http │ └── mooc_backend.http ├── infrastructure │ └── php │ │ ├── conf.d │ │ ├── apcu.ini │ │ ├── opcache.ini │ │ └── xdebug.ini │ │ └── php.ini └── prometheus │ └── prometheus.yml ├── phpmd.xml ├── phpstan.neon ├── phpunit.xml ├── psalm.xml ├── rector.php ├── src ├── Analytics │ └── DomainEvents │ │ ├── Application │ │ └── Store │ │ │ ├── DomainEventStorer.php │ │ │ └── StoreDomainEventOnOccurred.php │ │ └── Domain │ │ ├── AnalyticsDomainEvent.php │ │ ├── AnalyticsDomainEventAggregateId.php │ │ ├── AnalyticsDomainEventBody.php │ │ ├── AnalyticsDomainEventId.php │ │ ├── AnalyticsDomainEventName.php │ │ └── DomainEventsRepository.php ├── Backoffice │ ├── Auth │ │ ├── Application │ │ │ └── Authenticate │ │ │ │ ├── AuthenticateUserCommand.php │ │ │ │ ├── AuthenticateUserCommandHandler.php │ │ │ │ └── UserAuthenticator.php │ │ ├── Domain │ │ │ ├── AuthPassword.php │ │ │ ├── AuthRepository.php │ │ │ ├── AuthUser.php │ │ │ ├── AuthUsername.php │ │ │ ├── InvalidAuthCredentials.php │ │ │ └── InvalidAuthUsername.php │ │ └── Infrastructure │ │ │ └── Persistence │ │ │ └── InMemoryAuthRepository.php │ ├── Courses │ │ ├── Application │ │ │ ├── BackofficeCourseResponse.php │ │ │ ├── BackofficeCoursesResponse.php │ │ │ ├── Create │ │ │ │ ├── BackofficeCourseCreator.php │ │ │ │ └── CreateBackofficeCourseOnCourseCreated.php │ │ │ ├── SearchAll │ │ │ │ ├── AllBackofficeCoursesSearcher.php │ │ │ │ ├── SearchAllBackofficeCoursesQuery.php │ │ │ │ └── SearchAllBackofficeCoursesQueryHandler.php │ │ │ └── SearchByCriteria │ │ │ │ ├── BackofficeCoursesByCriteriaSearcher.php │ │ │ │ ├── SearchBackofficeCoursesByCriteriaQuery.php │ │ │ │ └── SearchBackofficeCoursesByCriteriaQueryHandler.php │ │ ├── Domain │ │ │ ├── BackofficeCourse.php │ │ │ └── BackofficeCourseRepository.php │ │ └── Infrastructure │ │ │ └── Persistence │ │ │ ├── Doctrine │ │ │ └── BackofficeCourse.orm.xml │ │ │ ├── ElasticsearchBackofficeCourseRepository.php │ │ │ ├── InMemoryCacheBackofficeCourseRepository.php │ │ │ └── MySqlBackofficeCourseRepository.php │ └── Shared │ │ └── Infrastructure │ │ └── Symfony │ │ └── DependencyInjection │ │ └── backoffice_services.yaml ├── Mooc │ ├── Courses │ │ ├── Application │ │ │ ├── Create │ │ │ │ ├── CourseCreator.php │ │ │ │ ├── CreateCourseCommand.php │ │ │ │ └── CreateCourseCommandHandler.php │ │ │ ├── Find │ │ │ │ └── CourseFinder.php │ │ │ └── Update │ │ │ │ └── CourseRenamer.php │ │ ├── Domain │ │ │ ├── Course.php │ │ │ ├── CourseCreatedDomainEvent.php │ │ │ ├── CourseDuration.php │ │ │ ├── CourseName.php │ │ │ ├── CourseNotExist.php │ │ │ └── CourseRepository.php │ │ └── Infrastructure │ │ │ ├── Cdc │ │ │ └── DatabaseMutationToCourseCreatedDomainEvent.php │ │ │ └── Persistence │ │ │ ├── Doctrine │ │ │ ├── Course.orm.xml │ │ │ ├── CourseDuration.orm.xml │ │ │ ├── CourseIdType.php │ │ │ └── CourseName.orm.xml │ │ │ ├── DoctrineCourseRepository.php │ │ │ └── FileCourseRepository.php │ ├── CoursesCounter │ │ ├── Application │ │ │ ├── Find │ │ │ │ ├── CoursesCounterFinder.php │ │ │ │ ├── CoursesCounterResponse.php │ │ │ │ ├── FindCoursesCounterQuery.php │ │ │ │ └── FindCoursesCounterQueryHandler.php │ │ │ └── Increment │ │ │ │ ├── CoursesCounterIncrementer.php │ │ │ │ └── IncrementCoursesCounterOnCourseCreated.php │ │ ├── Domain │ │ │ ├── CoursesCounter.php │ │ │ ├── CoursesCounterId.php │ │ │ ├── CoursesCounterIncrementedDomainEvent.php │ │ │ ├── CoursesCounterNotExist.php │ │ │ ├── CoursesCounterRepository.php │ │ │ └── CoursesCounterTotal.php │ │ └── Infrastructure │ │ │ └── Persistence │ │ │ ├── Doctrine │ │ │ ├── CourseCounterIdType.php │ │ │ ├── CourseIdsType.php │ │ │ ├── CoursesCounter.orm.xml │ │ │ └── CoursesCounterTotal.orm.xml │ │ │ └── DoctrineCoursesCounterRepository.php │ ├── Notifications │ │ └── Application │ │ │ ├── SendNewCommentReplyEmail │ │ │ └── .gitkeep │ │ │ ├── SendNewCommentReplyPush │ │ │ └── .gitkeep │ │ │ └── SendResetPasswordEmail │ │ │ └── .gitkeep │ ├── Shared │ │ ├── Domain │ │ │ ├── Courses │ │ │ │ └── CourseId.php │ │ │ └── Videos │ │ │ │ └── VideoUrl.php │ │ └── Infrastructure │ │ │ ├── Doctrine │ │ │ ├── DbalTypesSearcher.php │ │ │ ├── DoctrinePrefixesSearcher.php │ │ │ └── MoocEntityManagerFactory.php │ │ │ └── Symfony │ │ │ └── DependencyInjection │ │ │ └── mooc_services.yaml │ ├── Steps │ │ ├── Application │ │ │ └── Create │ │ │ │ ├── CreateVideoStepCommandHandler.php │ │ │ │ └── VideoStepCreator.php │ │ ├── Domain │ │ │ ├── Exercise │ │ │ │ ├── ExerciseStep.php │ │ │ │ └── ExerciseStepContent.php │ │ │ ├── Quiz │ │ │ │ ├── QuizStep.php │ │ │ │ └── QuizStepQuestion.php │ │ │ ├── Step.php │ │ │ ├── StepDuration.php │ │ │ ├── StepId.php │ │ │ ├── StepRepository.php │ │ │ ├── StepTitle.php │ │ │ └── Video │ │ │ │ ├── VideoStep.php │ │ │ │ └── VideoStepUrl.php │ │ └── Infrastructure │ │ │ └── Persistence │ │ │ ├── Doctrine │ │ │ ├── Exercise.ExerciseStep.orm.xml │ │ │ ├── Exercise.ExerciseStepContent.orm.xml │ │ │ ├── Quiz.QuizStep.orm.xml │ │ │ ├── QuizStepQuestionsType.php │ │ │ ├── Step.orm.xml │ │ │ ├── StepDuration.orm.xml │ │ │ ├── StepIdType.php │ │ │ ├── StepTitle.orm.xml │ │ │ ├── Video.VideoStep.orm.xml │ │ │ └── Video.VideoStepUrl.orm.xml │ │ │ └── MySqlStepRepository.php │ └── Videos │ │ ├── Application │ │ ├── Create │ │ │ ├── CreateVideoCommand.php │ │ │ ├── CreateVideoCommandHandler.php │ │ │ └── VideoCreator.php │ │ ├── Find │ │ │ ├── FindVideoQuery.php │ │ │ ├── FindVideoQueryHandler.php │ │ │ ├── VideoFinder.php │ │ │ ├── VideoResponse.php │ │ │ └── VideoResponseConverter.php │ │ ├── Trim │ │ │ ├── TrimVideoCommand.php │ │ │ ├── TrimVideoCommandHandler.php │ │ │ └── VideoTrimmer.php │ │ └── Update │ │ │ └── VideoTitleUpdater.php │ │ ├── Domain │ │ ├── Video.php │ │ ├── VideoCreatedDomainEvent.php │ │ ├── VideoFinder.php │ │ ├── VideoId.php │ │ ├── VideoNotFound.php │ │ ├── VideoRepository.php │ │ ├── VideoTitle.php │ │ ├── VideoType.php │ │ └── Videos.php │ │ └── Infrastructure │ │ └── Persistence │ │ ├── Doctrine │ │ ├── Video.orm.xml │ │ ├── VideoIdType.php │ │ ├── VideoTitle.orm.xml │ │ └── VideoType.orm.xml │ │ └── VideoRepositoryMySql.php ├── Retention │ ├── Campaign │ │ ├── Application │ │ │ ├── NewCourseAvailable │ │ │ │ ├── Schedule │ │ │ │ │ └── .gitkeep │ │ │ │ └── Trigger │ │ │ │ │ └── .gitkeep │ │ │ └── WelcomeUser │ │ │ │ └── Trigger │ │ │ │ └── .gitkeep │ │ ├── Domain │ │ │ └── .gitkeep │ │ └── Infrastructure │ │ │ └── .gitkeep │ ├── Email │ │ ├── Application │ │ │ ├── SendNewCourseAvailable │ │ │ │ └── .gitkeep │ │ │ └── SendWelcomeUser │ │ │ │ └── .gitkeep │ │ ├── Domain │ │ │ └── .gitkeep │ │ └── Infrastructure │ │ │ └── .gitkeep │ ├── Push │ │ ├── Application │ │ │ └── SendNewCourseAvailable │ │ │ │ └── .gitkeep │ │ ├── Domain │ │ │ └── .gitkeep │ │ └── Infrastructure │ │ │ └── .gitkeep │ └── Sms │ │ ├── Application │ │ └── .gitkeep │ │ ├── Domain │ │ └── .gitkeep │ │ └── Infrastructure │ │ └── .gitkeep └── Shared │ ├── Domain │ ├── Aggregate │ │ └── AggregateRoot.php │ ├── Assert.php │ ├── Bus │ │ ├── Command │ │ │ ├── Command.php │ │ │ ├── CommandBus.php │ │ │ └── CommandHandler.php │ │ ├── Event │ │ │ ├── DomainEvent.php │ │ │ ├── DomainEventSubscriber.php │ │ │ └── EventBus.php │ │ └── Query │ │ │ ├── Query.php │ │ │ ├── QueryBus.php │ │ │ ├── QueryHandler.php │ │ │ └── Response.php │ ├── Collection.php │ ├── Criteria │ │ ├── Criteria.php │ │ ├── Filter.php │ │ ├── FilterField.php │ │ ├── FilterOperator.php │ │ ├── FilterValue.php │ │ ├── Filters.php │ │ ├── Order.php │ │ ├── OrderBy.php │ │ └── OrderType.php │ ├── DomainError.php │ ├── Logger.php │ ├── Monitoring.php │ ├── RandomNumberGenerator.php │ ├── Second.php │ ├── SecondsInterval.php │ ├── Utils.php │ ├── UuidGenerator.php │ └── ValueObject │ │ ├── IntValueObject.php │ │ ├── SimpleUuid.php │ │ ├── StringValueObject.php │ │ └── Uuid.php │ └── Infrastructure │ ├── Bus │ ├── CallableFirstParameterExtractor.php │ ├── Command │ │ ├── CommandNotRegisteredError.php │ │ └── InMemorySymfonyCommandBus.php │ ├── Event │ │ ├── DomainEventJsonDeserializer.php │ │ ├── DomainEventJsonSerializer.php │ │ ├── DomainEventMapping.php │ │ ├── DomainEventSubscriberLocator.php │ │ ├── InMemory │ │ │ └── InMemorySymfonyEventBus.php │ │ ├── MySql │ │ │ ├── MySqlDoctrineDomainEventsConsumer.php │ │ │ └── MySqlDoctrineEventBus.php │ │ ├── RabbitMq │ │ │ ├── RabbitMqConfigurer.php │ │ │ ├── RabbitMqConnection.php │ │ │ ├── RabbitMqDomainEventsConsumer.php │ │ │ ├── RabbitMqEventBus.php │ │ │ ├── RabbitMqExchangeNameFormatter.php │ │ │ └── RabbitMqQueueNameFormatter.php │ │ └── WithMonitoring │ │ │ └── WithPrometheusMonitoringEventBus.php │ └── Query │ │ ├── InMemorySymfonyQueryBus.php │ │ └── QueryNotRegisteredError.php │ ├── Cdc │ ├── DatabaseMutationAction.php │ └── DatabaseMutationToDomainEvent.php │ ├── Doctrine │ ├── DatabaseConnections.php │ ├── Dbal │ │ ├── DbalCustomTypesRegistrar.php │ │ └── DoctrineCustomType.php │ └── DoctrineEntityManagerFactory.php │ ├── Elasticsearch │ ├── ElasticsearchClient.php │ └── ElasticsearchClientFactory.php │ ├── Logger │ └── MonologLogger.php │ ├── Monitoring │ └── PrometheusMonitor.php │ ├── Persistence │ ├── Doctrine │ │ ├── DoctrineCriteriaConverter.php │ │ ├── DoctrineRepository.php │ │ └── UuidType.php │ └── Elasticsearch │ │ ├── ElasticQueryGenerator.php │ │ ├── ElasticsearchCriteriaConverter.php │ │ └── ElasticsearchRepository.php │ ├── PhpRandomNumberGenerator.php │ ├── RamseyUuidGenerator.php │ └── Symfony │ ├── AddJsonBodyToRequestListener.php │ ├── ApiController.php │ ├── ApiExceptionListener.php │ ├── ApiExceptionsHttpStatusCodeMapping.php │ ├── BasicHttpAuthMiddleware.php │ ├── FlashSession.php │ └── WebController.php └── tests ├── Backoffice ├── Auth │ ├── Application │ │ └── Authenticate │ │ │ ├── AuthenticateUserCommandHandlerTest.php │ │ │ └── AuthenticateUserCommandMother.php │ ├── AuthModuleUnitTestCase.php │ └── Domain │ │ ├── AuthPasswordMother.php │ │ ├── AuthUserMother.php │ │ └── AuthUsernameMother.php ├── Courses │ ├── BackofficeCoursesModuleInfrastructureTestCase.php │ ├── Domain │ │ ├── BackofficeCourseCriteriaMother.php │ │ └── BackofficeCourseMother.php │ └── Infrastructure │ │ └── Persistence │ │ ├── ElasticsearchBackofficeCourseRepositoryTest.php │ │ └── MySqlBackofficeCourseRepositoryTest.php └── Shared │ └── Infraestructure │ └── PhpUnit │ ├── BackofficeContextInfrastructureTestCase.php │ └── BackofficeEnvironmentArranger.php ├── Mooc ├── Courses │ ├── Application │ │ ├── Create │ │ │ ├── CreateCourseCommandHandlerTest.php │ │ │ └── CreateCourseCommandMother.php │ │ └── Update │ │ │ └── CourseRenamerTest.php │ ├── CoursesModuleInfrastructureTestCase.php │ ├── CoursesModuleUnitTestCase.php │ ├── Domain │ │ ├── CourseCreatedDomainEventMother.php │ │ ├── CourseDurationMother.php │ │ ├── CourseIdMother.php │ │ ├── CourseMother.php │ │ └── CourseNameMother.php │ └── Infrastructure │ │ └── Persistence │ │ └── CourseRepositoryTest.php ├── CoursesCounter │ ├── Application │ │ ├── Find │ │ │ ├── CoursesCounterResponseMother.php │ │ │ └── FindCoursesCounterQueryHandlerTest.php │ │ └── Increment │ │ │ └── IncrementCoursesCounterOnCourseCreatedTest.php │ ├── CoursesCounterModuleUnitTestCase.php │ └── Domain │ │ ├── CoursesCounterIdMother.php │ │ ├── CoursesCounterIncrementedDomainEventMother.php │ │ ├── CoursesCounterMother.php │ │ └── CoursesCounterTotalMother.php ├── MoocArchitectureTest.php ├── Shared │ ├── Domain │ │ └── .gitkeep │ └── Infrastructure │ │ └── PhpUnit │ │ ├── MoocContextInfrastructureTestCase.php │ │ └── MoocEnvironmentArranger.php ├── Steps │ ├── Domain │ │ ├── Exercise │ │ │ ├── ExerciseStepContentMother.php │ │ │ └── ExerciseStepMother.php │ │ ├── Quiz │ │ │ ├── QuizStepMother.php │ │ │ └── QuizStepQuestionMother.php │ │ ├── StepDurationMother.php │ │ ├── StepIdMother.php │ │ ├── StepTitleMother.php │ │ └── Video │ │ │ ├── VideoStepMother.php │ │ │ └── VideoStepUrlMother.php │ ├── Infrastructure │ │ └── Persistence │ │ │ └── MySqlStepRepositoryTest.php │ └── StepsModuleInfrastructureTestCase.php └── Videos │ ├── Application │ └── .gitkeep │ ├── Domain │ └── .gitkeep │ └── Infrastructure │ └── .gitkeep └── Shared ├── Domain ├── Criteria │ ├── CriteriaMother.php │ ├── FilterFieldMother.php │ ├── FilterMother.php │ ├── FilterValueMother.php │ ├── FiltersMother.php │ ├── OrderByMother.php │ └── OrderMother.php ├── DuplicatorMother.php ├── IntegerMother.php ├── MotherCreator.php ├── RandomElementPicker.php ├── Repeater.php ├── TestUtils.php ├── UuidMother.php └── WordMother.php ├── Infrastructure ├── ArchitectureTest.php ├── Arranger │ └── EnvironmentArranger.php ├── Behat │ ├── ApiContext.php │ └── ApplicationFeatureContext.php ├── Bus │ ├── Command │ │ ├── FakeCommand.php │ │ └── InMemorySymfonyCommandBusTest.php │ ├── Event │ │ ├── MySql │ │ │ └── MySqlDoctrineEventBusTest.php │ │ └── RabbitMq │ │ │ ├── RabbitMqEventBusTest.php │ │ │ └── TestAllWorksOnRabbitMqEventsPublished.php │ └── Query │ │ ├── FakeQuery.php │ │ ├── FakeResponse.php │ │ └── InMemorySymfonyQueryBusTest.php ├── ConstantRandomNumberGenerator.php ├── Doctrine │ └── MySqlDatabaseCleaner.php ├── Elastic │ └── ElasticDatabaseCleaner.php ├── Mink │ ├── MinkHelper.php │ └── MinkSessionRequestHelper.php ├── Mockery │ └── CodelyTvMatcherIsSimilar.php └── PhpUnit │ ├── Comparator │ ├── AggregateRootArraySimilarComparator.php │ ├── AggregateRootSimilarComparator.php │ ├── DateTimeSimilarComparator.php │ ├── DateTimeStringSimilarComparator.php │ ├── DomainEventArraySimilarComparator.php │ └── DomainEventSimilarComparator.php │ ├── Constraint │ └── CodelyTvConstraintIsSimilar.php │ ├── InfrastructureTestCase.php │ └── UnitTestCase.php └── SharedArchitectureTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://bit.ly/CodelyTvPro 2 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: labeler 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | labeler: 7 | runs-on: ubuntu-latest 8 | name: Label the PR size 9 | steps: 10 | - uses: codelytv/pr-size-labeler@v1.8.1 11 | with: 12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 13 | xs_max_size: '10' 14 | s_max_size: '300' 15 | m_max_size: '600' 16 | l_max_size: '1400' 17 | fail_if_xl: 'true' 18 | files_to_ignore: 'composer.lock' 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.env.local 2 | /.env.*.local 3 | 4 | /apps/*/*/var/ 5 | !/apps/*/*/var/.gitkeep 6 | 7 | /apps/*/*/build/ 8 | !/apps/*/*/build/supervisor/.gitkeep 9 | 10 | /vendor/ 11 | .phpunit.result.cache 12 | 13 | /build 14 | 15 | .php-cs-fixer.cache 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.3-fpm-alpine 2 | WORKDIR /app 3 | 4 | RUN apk --update upgrade \ 5 | && apk add --no-cache autoconf automake make gcc g++ git bash icu-dev libzip-dev rabbitmq-c rabbitmq-c-dev linux-headers 6 | 7 | RUN pecl install apcu-5.1.23 && pecl install amqp-2.1.1 && pecl install xdebug-3.3.0 8 | 9 | RUN docker-php-ext-install -j$(nproc) \ 10 | bcmath \ 11 | opcache \ 12 | intl \ 13 | zip \ 14 | pdo_mysql 15 | 16 | RUN docker-php-ext-enable amqp apcu opcache 17 | 18 | RUN curl -sS https://get.symfony.com/cli/installer | bash -s - --install-dir /usr/local/bin 19 | 20 | COPY etc/infrastructure/php/ /usr/local/etc/php/ 21 | 22 | # allow non-root users have home 23 | RUN mkdir -p /opt/home 24 | RUN chmod 777 /opt/home 25 | ENV HOME /opt/home 26 | -------------------------------------------------------------------------------- /apps/backoffice/backend/config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 7 | FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['test' => true], 8 | // WouterJ\EloquentBundle\WouterJEloquentBundle::class => ['test' => true] 9 | ]; 10 | -------------------------------------------------------------------------------- /apps/backoffice/backend/config/routes/courses.yaml: -------------------------------------------------------------------------------- 1 | courses_get: 2 | path: /courses 3 | controller: CodelyTv\Apps\Backoffice\Backend\Controller\Courses\CoursesGetController 4 | defaults: { auth: false } 5 | methods: [GET] 6 | -------------------------------------------------------------------------------- /apps/backoffice/backend/config/routes/health-check.yaml: -------------------------------------------------------------------------------- 1 | health-check_get: 2 | path: /health-check 3 | controller: CodelyTv\Apps\Backoffice\Backend\Controller\HealthCheck\HealthCheckGetController 4 | methods: [GET] 5 | -------------------------------------------------------------------------------- /apps/backoffice/backend/config/routes/metrics.yaml: -------------------------------------------------------------------------------- 1 | metrics_get: 2 | path: /metrics 3 | controller: CodelyTv\Apps\Backoffice\Backend\Controller\Metrics\MetricsController 4 | methods: [GET] 5 | -------------------------------------------------------------------------------- /apps/backoffice/backend/config/services/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | #csrf_protection: true 4 | #http_method_override: true 5 | 6 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 7 | # Remove or comment this section to explicitly disable session support. 8 | session: 9 | handler_id: null 10 | cookie_secure: auto 11 | cookie_samesite: lax 12 | enabled: true 13 | 14 | #esi: true 15 | #fragments: true 16 | php_errors: 17 | log: true 18 | -------------------------------------------------------------------------------- /apps/backoffice/backend/config/services_test.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | 4 | services: 5 | _defaults: 6 | autoconfigure: true 7 | autowire: true 8 | 9 | CodelyTv\Tests\: 10 | resource: '../../../../tests' 11 | 12 | # -- IMPLEMENTATIONS SELECTOR -- 13 | CodelyTv\Shared\Domain\Bus\Event\EventBus: '@CodelyTv\Shared\Infrastructure\Bus\Event\InMemory\InMemorySymfonyEventBus' 14 | -------------------------------------------------------------------------------- /apps/backoffice/backend/public/index.php: -------------------------------------------------------------------------------- 1 | handle($request); 31 | $response->send(); 32 | $kernel->terminate($request, $response); 33 | -------------------------------------------------------------------------------- /apps/backoffice/backend/src/Controller/HealthCheck/HealthCheckGetController.php: -------------------------------------------------------------------------------- 1 | 'ok', 17 | ] 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/backoffice/backend/src/Controller/Metrics/MetricsController.php: -------------------------------------------------------------------------------- 1 | render($this->monitor->registry()->getMetricFamilySamples()); 20 | 21 | return new Response($result, 200, ['Content-Type' => RenderTextFormat::MIME_TYPE]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/backoffice/backend/var/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/apps/backoffice/backend/var/.gitkeep -------------------------------------------------------------------------------- /apps/backoffice/frontend/config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 7 | FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['test' => true], 8 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 9 | // WouterJ\EloquentBundle\WouterJEloquentBundle::class => ['test' => true] 10 | ]; 11 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/config/routes/api_courses.yaml: -------------------------------------------------------------------------------- 1 | api_courses_get: 2 | path: /api/courses 3 | controller: CodelyTv\Apps\Backoffice\Frontend\Controller\Courses\ApiCoursesGetController 4 | methods: [GET] 5 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/config/routes/courses.yaml: -------------------------------------------------------------------------------- 1 | courses_get: 2 | path: /courses 3 | controller: CodelyTv\Apps\Backoffice\Frontend\Controller\Courses\CoursesGetWebController 4 | methods: [GET] 5 | 6 | courses_post: 7 | path: /courses 8 | controller: CodelyTv\Apps\Backoffice\Frontend\Controller\Courses\CoursesPostWebController 9 | methods: [POST] 10 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/config/routes/health-check.yaml: -------------------------------------------------------------------------------- 1 | health-check_get: 2 | path: /health-check 3 | controller: CodelyTv\Apps\Backoffice\Frontend\Controller\HealthCheck\HealthCheckGetController 4 | methods: [GET] 5 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/config/routes/home.yaml: -------------------------------------------------------------------------------- 1 | home_get: 2 | path: / 3 | controller: CodelyTv\Apps\Backoffice\Frontend\Controller\Home\HomeGetWebController 4 | methods: [GET] 5 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/config/routes/metrics.yaml: -------------------------------------------------------------------------------- 1 | metrics_get: 2 | path: /metrics 3 | controller: CodelyTv\Apps\Backoffice\Frontend\Controller\Metrics\MetricsController 4 | methods: [GET] 5 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/config/services/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | #csrf_protection: true 4 | #http_method_override: true 5 | 6 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 7 | # Remove or comment this section to explicitly disable session support. 8 | session: 9 | handler_id: null 10 | cookie_secure: auto 11 | cookie_samesite: lax 12 | enabled: true 13 | 14 | #esi: true 15 | #fragments: true 16 | php_errors: 17 | log: true 18 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/config/services_test.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | 4 | services: 5 | _defaults: 6 | autoconfigure: true 7 | autowire: true 8 | 9 | CodelyTv\Tests\: 10 | resource: '../../../../tests' 11 | 12 | # -- IMPLEMENTATIONS SELECTOR -- 13 | CodelyTv\Shared\Domain\Bus\Event\EventBus: '@CodelyTv\Shared\Infrastructure\Bus\Event\InMemory\InMemorySymfonyEventBus' 14 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/apps/backoffice/frontend/public/images/logo.png -------------------------------------------------------------------------------- /apps/backoffice/frontend/public/index.php: -------------------------------------------------------------------------------- 1 | handle($request); 31 | $response->send(); 32 | $kernel->terminate($request, $response); 33 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/src/Command/ImportCoursesToElasticsearchCommand.php: -------------------------------------------------------------------------------- 1 | mySqlRepository->searchAll(); 25 | 26 | foreach ($courses as $course) { 27 | $this->elasticRepository->save($course); 28 | } 29 | 30 | return 0; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/src/Controller/Courses/CoursesGetWebController.php: -------------------------------------------------------------------------------- 1 | ask(new FindCoursesCounterQuery()); 20 | 21 | return $this->render( 22 | 'pages/courses/courses.html.twig', 23 | [ 24 | 'title' => 'Courses', 25 | 'description' => 'Courses CodelyTV - Backoffice', 26 | 'courses_counter' => $coursesCounterResponse->total(), 27 | 'new_course_id' => SimpleUuid::random()->value(), 28 | ] 29 | ); 30 | } 31 | 32 | protected function exceptions(): array 33 | { 34 | return []; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/src/Controller/HealthCheck/HealthCheckGetController.php: -------------------------------------------------------------------------------- 1 | 'ok', 17 | ] 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/src/Controller/Home/HomeGetWebController.php: -------------------------------------------------------------------------------- 1 | render('pages/home.html.twig', [ 16 | 'title' => 'Welcome', 17 | 'description' => 'CodelyTV - Backoffice', 18 | ]); 19 | } 20 | 21 | protected function exceptions(): array 22 | { 23 | return []; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/src/Controller/Metrics/MetricsController.php: -------------------------------------------------------------------------------- 1 | render($this->monitor->registry()->getMetricFamilySamples()); 20 | 21 | return new Response($result, 200, ['Content-Type' => RenderTextFormat::MIME_TYPE]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/templates/pages/courses/courses.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'master.html.twig' %} 2 | 3 | {% block page_title %}Cursos{% endblock %} 4 | 5 | {% block main %} 6 |
7 | Sunset in the mountains 8 |
9 |
Cursos
10 |

11 | Actualmente CodelyTV Pro cuenta con {{ courses_counter }} cursos. 12 |

13 |
14 |
15 | {{ include('pages/courses/partials/new_course_form.html.twig') }} 16 |
17 |
18 | {{ include('pages/courses/partials/list_courses.html.twig') }} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/templates/pages/home.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'master.html.twig' %} 2 | 3 | {% block page_title %}HOME{% endblock %} 4 | 5 | {% block main %} 6 | HOLIII HOME 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/templates/partials/footer.html.twig: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /apps/backoffice/frontend/var/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/apps/backoffice/frontend/var/.gitkeep -------------------------------------------------------------------------------- /apps/bootstrap.php: -------------------------------------------------------------------------------- 1 | loadEnv($rootPath . '/.env'); 12 | 13 | $_SERVER += $_ENV; 14 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 15 | $_SERVER['APP_DEBUG'] ??= $_ENV['APP_DEBUG'] ?? $_SERVER['APP_ENV'] !== 'prod'; 16 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = 17 | (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 18 | -------------------------------------------------------------------------------- /apps/mooc/backend/config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 7 | FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['test' => true], 8 | // WouterJ\EloquentBundle\WouterJEloquentBundle::class => ['test' => true] 9 | ]; 10 | -------------------------------------------------------------------------------- /apps/mooc/backend/config/routes/courses.yaml: -------------------------------------------------------------------------------- 1 | courses_put: 2 | path: /courses/{id} 3 | controller: CodelyTv\Apps\Mooc\Backend\Controller\Courses\CoursesPutController 4 | methods: [PUT] 5 | -------------------------------------------------------------------------------- /apps/mooc/backend/config/routes/courses_counter.yaml: -------------------------------------------------------------------------------- 1 | courses_counter_get: 2 | path: /courses-counter 3 | controller: CodelyTv\Apps\Mooc\Backend\Controller\CoursesCounter\CoursesCounterGetController 4 | methods: [GET] 5 | -------------------------------------------------------------------------------- /apps/mooc/backend/config/routes/health-check.yaml: -------------------------------------------------------------------------------- 1 | health-check_get: 2 | path: /health-check 3 | controller: CodelyTv\Apps\Mooc\Backend\Controller\HealthCheck\HealthCheckGetController 4 | methods: [GET] 5 | -------------------------------------------------------------------------------- /apps/mooc/backend/config/routes/metrics.yaml: -------------------------------------------------------------------------------- 1 | metrics_get: 2 | path: /metrics 3 | controller: CodelyTv\Apps\Mooc\Backend\Controller\Metrics\MetricsController 4 | methods: [GET] 5 | -------------------------------------------------------------------------------- /apps/mooc/backend/config/services/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | #csrf_protection: true 4 | #http_method_override: true 5 | 6 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 7 | # Remove or comment this section to explicitly disable session support. 8 | session: 9 | handler_id: ~ 10 | cookie_secure: auto 11 | cookie_samesite: lax 12 | 13 | #esi: true 14 | #fragments: true 15 | php_errors: 16 | log: true 17 | -------------------------------------------------------------------------------- /apps/mooc/backend/config/services_test.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | 4 | services: 5 | _defaults: 6 | autoconfigure: true 7 | autowire: true 8 | 9 | CodelyTv\Tests\: 10 | resource: '../../../../tests' 11 | 12 | # Instance selector 13 | CodelyTv\Shared\Domain\RandomNumberGenerator: '@CodelyTv\Tests\Shared\Infrastructure\ConstantRandomNumberGenerator' 14 | # CodelyTv\Shared\Domain\Bus\Event\EventBus: '@CodelyTv\Shared\Infrastructure\Bus\Event\InMemory\InMemorySymfonyEventBus' 15 | -------------------------------------------------------------------------------- /apps/mooc/backend/public/index.php: -------------------------------------------------------------------------------- 1 | handle($request); 31 | $response->send(); 32 | $kernel->terminate($request, $response); 33 | -------------------------------------------------------------------------------- /apps/mooc/backend/src/Command/DomainEvents/RabbitMq/ConfigureRabbitMqCommand.php: -------------------------------------------------------------------------------- 1 | configurer->configure($this->exchangeName, ...iterator_to_array($this->subscribers)); 31 | 32 | return 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/mooc/backend/src/Controller/Courses/CoursesPutController.php: -------------------------------------------------------------------------------- 1 | dispatch( 17 | new CreateCourseCommand( 18 | $id, 19 | (string) $request->request->get('name'), 20 | (string) $request->request->get('duration') 21 | ) 22 | ); 23 | 24 | return new Response('', Response::HTTP_CREATED); 25 | } 26 | 27 | protected function exceptions(): array 28 | { 29 | return []; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/mooc/backend/src/Controller/CoursesCounter/CoursesCounterGetController.php: -------------------------------------------------------------------------------- 1 | ask(new FindCoursesCounterQuery()); 20 | 21 | return new JsonResponse( 22 | [ 23 | 'total' => $response->total(), 24 | ] 25 | ); 26 | } 27 | 28 | protected function exceptions(): array 29 | { 30 | return [ 31 | CoursesCounterNotExist::class => Response::HTTP_NOT_FOUND, 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/mooc/backend/src/Controller/HealthCheck/HealthCheckGetController.php: -------------------------------------------------------------------------------- 1 | 'ok', 20 | 'rand' => $this->generator->generate(), 21 | ] 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/mooc/backend/src/Controller/Metrics/MetricsController.php: -------------------------------------------------------------------------------- 1 | render($this->monitor->registry()->getMetricFamilySamples()); 20 | 21 | return new Response($result, 200, ['Content-Type' => RenderTextFormat::MIME_TYPE]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/mooc/backend/tests/features/courses/course_put.feature: -------------------------------------------------------------------------------- 1 | Feature: Create a new course 2 | In order to have courses on the platform 3 | As a user with admin permissions 4 | I want to create a new course 5 | 6 | Scenario: A valid non existing course 7 | Given I send a PUT request to "/courses/1aab45ba-3c7a-4344-8936-78466eca77fa" with body: 8 | """ 9 | { 10 | "name": "The best course", 11 | "duration": "5 hours" 12 | } 13 | """ 14 | Then the response status code should be 201 15 | And the response should be empty 16 | -------------------------------------------------------------------------------- /apps/mooc/backend/tests/features/health_check/health_check_get.feature: -------------------------------------------------------------------------------- 1 | Feature: Api status 2 | In order to know the server is up and running 3 | As a health check 4 | I want to check the api status 5 | 6 | Scenario: Check the api status 7 | Given I send a GET request to "/health-check" 8 | Then the response content should be: 9 | """ 10 | { 11 | "mooc-backend": "ok", 12 | "rand": 1 13 | } 14 | """ 15 | -------------------------------------------------------------------------------- /apps/mooc/backend/tests/mooc_backend.yml: -------------------------------------------------------------------------------- 1 | mooc_backend: 2 | extensions: 3 | FriendsOfBehat\SymfonyExtension: 4 | kernel: 5 | class: CodelyTv\Apps\Mooc\Backend\MoocBackendKernel 6 | bootstrap: apps/bootstrap.php 7 | Behat\MinkExtension: 8 | sessions: 9 | symfony: 10 | symfony: ~ 11 | base_url: '' 12 | 13 | suites: 14 | health_check: 15 | paths: [ apps/mooc/backend/tests/features/health_check ] 16 | contexts: 17 | - CodelyTv\Tests\Shared\Infrastructure\Behat\ApiContext 18 | 19 | courses: 20 | paths: [ apps/mooc/backend/tests/features/courses ] 21 | contexts: 22 | - CodelyTv\Tests\Shared\Infrastructure\Behat\ApplicationFeatureContext 23 | - CodelyTv\Tests\Shared\Infrastructure\Behat\ApiContext 24 | 25 | courses_counter: 26 | paths: [ apps/mooc/backend/tests/features/courses_counter ] 27 | contexts: 28 | - CodelyTv\Tests\Shared\Infrastructure\Behat\ApplicationFeatureContext 29 | - CodelyTv\Tests\Shared\Infrastructure\Behat\ApiContext 30 | -------------------------------------------------------------------------------- /apps/mooc/backend/var/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/apps/mooc/backend/var/.gitkeep -------------------------------------------------------------------------------- /apps/mooc/frontend/src/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/apps/mooc/frontend/src/.gitkeep -------------------------------------------------------------------------------- /apps/mooc/frontend/var/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/apps/mooc/frontend/var/.gitkeep -------------------------------------------------------------------------------- /behat.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - apps/mooc/backend/tests/mooc_backend.yml 3 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([__DIR__ . '/apps', __DIR__ . '/src', __DIR__ . '/tests', ]); 11 | 12 | $ecsConfig->sets([CodingStyle::DEFAULT]); 13 | 14 | $ecsConfig->skip([ 15 | FinalClassFixer::class => [ 16 | __DIR__ . '/apps/backoffice/backend/src/BackofficeBackendKernel.php', 17 | __DIR__ . '/apps/backoffice/frontend/src/BackofficeFrontendKernel.php', 18 | __DIR__ . '/apps/mooc/backend/src/MoocBackendKernel.php', 19 | __DIR__ . '/src/Shared/Infrastructure/Bus/Event/InMemory/InMemorySymfonyEventBus.php', 20 | ], 21 | __DIR__ . '/apps/backoffice/backend/var', 22 | __DIR__ . '/apps/backoffice/frontend/var', 23 | __DIR__ . '/apps/mooc/backend/var', 24 | __DIR__ . '/apps/mooc/frontend/var', 25 | ]); 26 | }; 27 | -------------------------------------------------------------------------------- /etc/databases/backoffice/courses.json: -------------------------------------------------------------------------------- 1 | { 2 | "mappings": { 3 | "courses": { 4 | "properties": { 5 | "id": { 6 | "type": "keyword", 7 | "index": true 8 | }, 9 | "name": { 10 | "type": "text", 11 | "index": true 12 | }, 13 | "duration": { 14 | "type": "text", 15 | "index": true 16 | } 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /etc/endpoints/backoffice_frontend.http: -------------------------------------------------------------------------------- 1 | # ELASTIC - Search 2 | POST localhost:9200/backoffice_courses/_search 3 | Content-Type: application/json 4 | 5 | { 6 | "query": { 7 | "term": { 8 | "name": "Pepe" 9 | } 10 | } 11 | } 12 | 13 | ### 14 | # ELASTIC - Search 15 | POST localhost:9200/backoffice_courses/_search 16 | Content-Type: application/json 17 | 18 | ### 19 | 20 | -------------------------------------------------------------------------------- /etc/endpoints/mooc_backend.http: -------------------------------------------------------------------------------- 1 | # Create a course 2 | PUT localhost:8030/courses/{{$uuid}} 3 | Content-Type: application/json 4 | 5 | { 6 | "name": "The best course", 7 | "duration": "5 hours" 8 | } 9 | 10 | ### 11 | -------------------------------------------------------------------------------- /etc/infrastructure/php/conf.d/apcu.ini: -------------------------------------------------------------------------------- 1 | apc.enable_cli=1 2 | apc.enabled=1 3 | apc.shm_segments=1 4 | apc.shm_size=256M 5 | apc.num_files_hint=7000 6 | apc.user_entries_hint=4096 7 | apc.ttl=7200 8 | apc.user_ttl=7200 9 | apc.gc_ttl=3600 10 | apc.max_file_size=1M 11 | apc.stat=1 12 | -------------------------------------------------------------------------------- /etc/infrastructure/php/conf.d/opcache.ini: -------------------------------------------------------------------------------- 1 | opcache.memory_consumption=128 2 | opcache.interned_strings_buffer=8 3 | opcache.max_accelerated_files=4000 4 | opcache.revalidate_freq=60 5 | opcache.fast_shutdown=1 6 | opcache.enable_cli=1 7 | -------------------------------------------------------------------------------- /etc/infrastructure/php/conf.d/xdebug.ini: -------------------------------------------------------------------------------- 1 | zend_extension = xdebug.so 2 | 3 | ;Debugging 4 | xdebug.mode=debug 5 | xdebug.start_with_request = yes 6 | xdebug.discover_client_host = yes 7 | xdebug.client_port = 9001 8 | xdebug.client_host = host.docker.internal 9 | 10 | ;Profiling 11 | xdebug.mode=profile 12 | xdebug.start_with_request=trigger 13 | 14 | xdebug.output_dir = "/tmp/xdebug" 15 | xdebug.max_nesting_level = 500 16 | -------------------------------------------------------------------------------- /etc/infrastructure/php/php.ini: -------------------------------------------------------------------------------- 1 | date.timezone = "UTC" 2 | html_errors = "On" 3 | display_errors = "On" 4 | error_reporting = E_ALL 5 | -------------------------------------------------------------------------------- /etc/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | scrape_configs: 2 | 3 | - job_name: 'prometheus' 4 | scrape_interval: 5s 5 | static_configs: 6 | - targets: ['localhost:9090'] 7 | 8 | - job_name: 'backoffice_backend' 9 | scrape_interval: 5s 10 | static_configs: 11 | - targets: ['codelytv-php_ddd_skeleton-backoffice_backend-php:8040'] 12 | 13 | - job_name: 'backoffice_frontend' 14 | scrape_interval: 5s 15 | static_configs: 16 | - targets: ['codelytv-php_ddd_skeleton-backoffice_frontend-php:8041'] 17 | 18 | - job_name: 'mooc_backend' 19 | scrape_interval: 5s 20 | static_configs: 21 | - targets: ['codelytv-php_ddd_skeleton-mooc_backend-php:8030'] 22 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpat/phpat/extension.neon 3 | 4 | parameters: 5 | level: 0 6 | paths: 7 | - ./apps 8 | - ./src 9 | - ./tests 10 | excludePaths: 11 | - ./apps/backoffice/backend/var 12 | - ./apps/backoffice/frontend/var 13 | - ./apps/mooc/backend/var 14 | - ./apps/mooc/frontend/var 15 | 16 | services: 17 | - 18 | class: CodelyTv\Tests\Shared\SharedArchitectureTest 19 | tags: 20 | - phpat.test 21 | 22 | - 23 | class: CodelyTv\Tests\Mooc\MoocArchitectureTest 24 | tags: 25 | - phpat.test 26 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 11 | __DIR__ . '/apps', 12 | __DIR__ . '/src', 13 | __DIR__ . '/tests', 14 | ]); 15 | 16 | $rectorConfig->sets([ 17 | LevelSetList::UP_TO_PHP_82, 18 | SetList::TYPE_DECLARATION 19 | ]); 20 | 21 | $rectorConfig->skip([ 22 | __DIR__ . '/apps/backoffice/backend/var', 23 | __DIR__ . '/apps/backoffice/frontend/var', 24 | __DIR__ . '/apps/mooc/backend/var', 25 | __DIR__ . '/apps/mooc/frontend/var', 26 | ]); 27 | }; 28 | -------------------------------------------------------------------------------- /src/Analytics/DomainEvents/Application/Store/DomainEventStorer.php: -------------------------------------------------------------------------------- 1 | repository->save($domainEvent); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Analytics/DomainEvents/Application/Store/StoreDomainEventOnOccurred.php: -------------------------------------------------------------------------------- 1 | eventId()); 26 | $aggregateId = new AnalyticsDomainEventAggregateId($event->aggregateId()); 27 | $name = new AnalyticsDomainEventName($event::eventName()); 28 | $body = new AnalyticsDomainEventBody($event->toPrimitives()); 29 | 30 | $this->storer->store($id, $aggregateId, $name, $body); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Analytics/DomainEvents/Domain/AnalyticsDomainEvent.php: -------------------------------------------------------------------------------- 1 | value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Analytics/DomainEvents/Domain/AnalyticsDomainEventId.php: -------------------------------------------------------------------------------- 1 | username; 16 | } 17 | 18 | public function password(): string 19 | { 20 | return $this->password; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Backoffice/Auth/Application/Authenticate/AuthenticateUserCommandHandler.php: -------------------------------------------------------------------------------- 1 | username()); 18 | $password = new AuthPassword($command->password()); 19 | 20 | $this->authenticator->authenticate($username, $password); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Backoffice/Auth/Application/Authenticate/UserAuthenticator.php: -------------------------------------------------------------------------------- 1 | repository->search($username); 21 | 22 | if ($auth === null) { 23 | throw new InvalidAuthUsername($username); 24 | } 25 | 26 | $this->ensureCredentialsAreValid($auth, $password); 27 | } 28 | 29 | private function ensureCredentialsAreValid(AuthUser $auth, AuthPassword $password): void 30 | { 31 | if (!$auth->passwordMatches($password)) { 32 | throw new InvalidAuthCredentials($auth->username()); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Backoffice/Auth/Domain/AuthPassword.php: -------------------------------------------------------------------------------- 1 | value() === $other->value(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Backoffice/Auth/Domain/AuthRepository.php: -------------------------------------------------------------------------------- 1 | password->isEquals($password); 14 | } 15 | 16 | public function username(): AuthUsername 17 | { 18 | return $this->username; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Backoffice/Auth/Domain/AuthUsername.php: -------------------------------------------------------------------------------- 1 | are invalid', $username->value())); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Backoffice/Auth/Domain/InvalidAuthUsername.php: -------------------------------------------------------------------------------- 1 | does not exists', $username->value())); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Backoffice/Auth/Infrastructure/Persistence/InMemoryAuthRepository.php: -------------------------------------------------------------------------------- 1 | 'barbitas', 18 | 'rafa' => 'pelazo', 19 | ]; 20 | 21 | public function search(AuthUsername $username): ?AuthUser 22 | { 23 | $password = get($username->value(), self::USERS); 24 | 25 | return $password !== null ? new AuthUser($username, new AuthPassword($password)) : null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Application/BackofficeCourseResponse.php: -------------------------------------------------------------------------------- 1 | id; 14 | } 15 | 16 | public function name(): string 17 | { 18 | return $this->name; 19 | } 20 | 21 | public function duration(): string 22 | { 23 | return $this->duration; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Application/BackofficeCoursesResponse.php: -------------------------------------------------------------------------------- 1 | courses = $courses; 16 | } 17 | 18 | public function courses(): array 19 | { 20 | return $this->courses; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Application/Create/BackofficeCourseCreator.php: -------------------------------------------------------------------------------- 1 | repository->save(new BackofficeCourse($id, $name, $duration)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Application/Create/CreateBackofficeCourseOnCourseCreated.php: -------------------------------------------------------------------------------- 1 | creator->create($event->aggregateId(), $event->name(), $event->duration()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Application/SearchAll/AllBackofficeCoursesSearcher.php: -------------------------------------------------------------------------------- 1 | toResponse(), $this->repository->searchAll())); 21 | } 22 | 23 | private function toResponse(): callable 24 | { 25 | return static fn (BackofficeCourse $course): BackofficeCourseResponse => new BackofficeCourseResponse( 26 | $course->id(), 27 | $course->name(), 28 | $course->duration() 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Application/SearchAll/SearchAllBackofficeCoursesQuery.php: -------------------------------------------------------------------------------- 1 | searcher->searchAll(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Application/SearchByCriteria/SearchBackofficeCoursesByCriteriaQuery.php: -------------------------------------------------------------------------------- 1 | filters; 22 | } 23 | 24 | public function orderBy(): ?string 25 | { 26 | return $this->orderBy; 27 | } 28 | 29 | public function order(): ?string 30 | { 31 | return $this->order; 32 | } 33 | 34 | public function limit(): ?int 35 | { 36 | return $this->limit; 37 | } 38 | 39 | public function offset(): ?int 40 | { 41 | return $this->offset; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Application/SearchByCriteria/SearchBackofficeCoursesByCriteriaQueryHandler.php: -------------------------------------------------------------------------------- 1 | filters()); 19 | $order = Order::fromValues($query->orderBy(), $query->order()); 20 | 21 | return $this->searcher->search($filters, $order, $query->limit(), $query->offset()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Domain/BackofficeCourse.php: -------------------------------------------------------------------------------- 1 | $this->id, 22 | 'name' => $this->name, 23 | 'duration' => $this->duration, 24 | ]; 25 | } 26 | 27 | public function id(): string 28 | { 29 | return $this->id; 30 | } 31 | 32 | public function name(): string 33 | { 34 | return $this->name; 35 | } 36 | 37 | public function duration(): string 38 | { 39 | return $this->duration; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Domain/BackofficeCourseRepository.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Backoffice/Courses/Infrastructure/Persistence/MySqlBackofficeCourseRepository.php: -------------------------------------------------------------------------------- 1 | persist($course); 18 | } 19 | 20 | public function searchAll(): array 21 | { 22 | return $this->repository(BackofficeCourse::class)->findAll(); 23 | } 24 | 25 | public function matching(Criteria $criteria): array 26 | { 27 | $doctrineCriteria = DoctrineCriteriaConverter::convert($criteria); 28 | 29 | return $this->repository(BackofficeCourse::class)->matching($doctrineCriteria)->toArray(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Backoffice/Shared/Infrastructure/Symfony/DependencyInjection/backoffice_services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | # Databases 3 | # @todo this should be from backoffice, no mooc 4 | Doctrine\ORM\EntityManager: 5 | factory: [ CodelyTv\Mooc\Shared\Infrastructure\Doctrine\MoocEntityManagerFactory, create ] 6 | arguments: 7 | - driver: '%env(MOOC_DATABASE_DRIVER)%' 8 | host: '%env(MOOC_DATABASE_HOST)%' 9 | port: '%env(MOOC_DATABASE_PORT)%' 10 | dbname: '%env(MOOC_DATABASE_NAME)%' 11 | user: '%env(MOOC_DATABASE_USER)%' 12 | password: '%env(MOOC_DATABASE_PASSWORD)%' 13 | - '%env(APP_ENV)%' 14 | tags: 15 | - { name: codely.database_connection } 16 | public: true 17 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Application/Create/CourseCreator.php: -------------------------------------------------------------------------------- 1 | repository->save($course); 23 | $this->bus->publish(...$course->pullDomainEvents()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Application/Create/CreateCourseCommand.php: -------------------------------------------------------------------------------- 1 | id; 16 | } 17 | 18 | public function name(): string 19 | { 20 | return $this->name; 21 | } 22 | 23 | public function duration(): string 24 | { 25 | return $this->duration; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Application/Create/CreateCourseCommandHandler.php: -------------------------------------------------------------------------------- 1 | id()); 19 | $name = new CourseName($command->name()); 20 | $duration = new CourseDuration($command->duration()); 21 | 22 | $this->creator->__invoke($id, $name, $duration); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Application/Find/CourseFinder.php: -------------------------------------------------------------------------------- 1 | repository->search($id); 19 | 20 | if ($course === null) { 21 | throw new CourseNotExist($id); 22 | } 23 | 24 | return $course; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Application/Update/CourseRenamer.php: -------------------------------------------------------------------------------- 1 | finder = new CourseFinder($repository); 20 | } 21 | 22 | public function __invoke(CourseId $id, CourseName $newName): void 23 | { 24 | $course = $this->finder->__invoke($id); 25 | 26 | $course->rename($newName); 27 | 28 | $this->repository->save($course); 29 | $this->bus->publish(...$course->pullDomainEvents()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Domain/Course.php: -------------------------------------------------------------------------------- 1 | record(new CourseCreatedDomainEvent($id->value(), $name->value(), $duration->value())); 19 | 20 | return $course; 21 | } 22 | 23 | public function id(): CourseId 24 | { 25 | return $this->id; 26 | } 27 | 28 | public function name(): CourseName 29 | { 30 | return $this->name; 31 | } 32 | 33 | public function duration(): CourseDuration 34 | { 35 | return $this->duration; 36 | } 37 | 38 | public function rename(CourseName $newName): void 39 | { 40 | $this->name = $newName; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Domain/CourseCreatedDomainEvent.php: -------------------------------------------------------------------------------- 1 | $this->name, 39 | 'duration' => $this->duration, 40 | ]; 41 | } 42 | 43 | public function name(): string 44 | { 45 | return $this->name; 46 | } 47 | 48 | public function duration(): string 49 | { 50 | return $this->duration; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Domain/CourseDuration.php: -------------------------------------------------------------------------------- 1 | does not exist', $this->id->value()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Domain/CourseRepository.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Infrastructure/Persistence/Doctrine/CourseDuration.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Infrastructure/Persistence/Doctrine/CourseIdType.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Infrastructure/Persistence/DoctrineCourseRepository.php: -------------------------------------------------------------------------------- 1 | persist($course); 17 | } 18 | 19 | public function search(CourseId $id): ?Course 20 | { 21 | return $this->repository(Course::class)->find($id); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Mooc/Courses/Infrastructure/Persistence/FileCourseRepository.php: -------------------------------------------------------------------------------- 1 | fileName($course->id()->value()), serialize($course)); 18 | } 19 | 20 | public function search(CourseId $id): ?Course 21 | { 22 | return file_exists($this->fileName($id->value())) 23 | ? unserialize(file_get_contents($this->fileName($id->value()))) 24 | : null; 25 | } 26 | 27 | private function fileName(string $id): string 28 | { 29 | return sprintf('%s.%s.repo', self::FILE_PATH, $id); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Mooc/CoursesCounter/Application/Find/CoursesCounterFinder.php: -------------------------------------------------------------------------------- 1 | repository->search(); 17 | 18 | if ($counter === null) { 19 | throw new CoursesCounterNotExist(); 20 | } 21 | 22 | return new CoursesCounterResponse($counter->total()->value()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mooc/CoursesCounter/Application/Find/CoursesCounterResponse.php: -------------------------------------------------------------------------------- 1 | total; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mooc/CoursesCounter/Application/Find/FindCoursesCounterQuery.php: -------------------------------------------------------------------------------- 1 | finder->__invoke(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mooc/CoursesCounter/Application/Increment/IncrementCoursesCounterOnCourseCreated.php: -------------------------------------------------------------------------------- 1 | aggregateId()); 25 | 26 | apply($this->incrementer, [$courseId]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Mooc/CoursesCounter/Domain/CoursesCounterId.php: -------------------------------------------------------------------------------- 1 | $this->total, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Mooc/CoursesCounter/Domain/CoursesCounterNotExist.php: -------------------------------------------------------------------------------- 1 | value() + 1); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Mooc/CoursesCounter/Infrastructure/Persistence/Doctrine/CourseCounterIdType.php: -------------------------------------------------------------------------------- 1 | $id->value(), $value), $platform); 29 | } 30 | 31 | public function convertToPHPValue($value, AbstractPlatform $platform): array 32 | { 33 | $scalars = parent::convertToPHPValue($value, $platform); 34 | 35 | return map(fn (string $value): CourseId => new CourseId($value), $scalars); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Mooc/CoursesCounter/Infrastructure/Persistence/Doctrine/CoursesCounter.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Mooc/CoursesCounter/Infrastructure/Persistence/Doctrine/CoursesCounterTotal.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Mooc/CoursesCounter/Infrastructure/Persistence/DoctrineCoursesCounterRepository.php: -------------------------------------------------------------------------------- 1 | persist($counter); 16 | } 17 | 18 | public function search(): ?CoursesCounter 19 | { 20 | return $this->repository(CoursesCounter::class)->findOneBy([]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Mooc/Notifications/Application/SendNewCommentReplyEmail/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Mooc/Notifications/Application/SendNewCommentReplyEmail/.gitkeep -------------------------------------------------------------------------------- /src/Mooc/Notifications/Application/SendNewCommentReplyPush/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Mooc/Notifications/Application/SendNewCommentReplyPush/.gitkeep -------------------------------------------------------------------------------- /src/Mooc/Notifications/Application/SendResetPasswordEmail/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Mooc/Notifications/Application/SendResetPasswordEmail/.gitkeep -------------------------------------------------------------------------------- /src/Mooc/Shared/Domain/Courses/CourseId.php: -------------------------------------------------------------------------------- 1 | ensureIsValidUrl($value); 15 | 16 | parent::__construct($value); 17 | } 18 | 19 | private function ensureIsValidUrl(string $url): void 20 | { 21 | if (filter_var($url, FILTER_VALIDATE_URL) === false) { 22 | throw new InvalidArgumentException(sprintf('The url <%s> is not well formatted', $url)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Mooc/Shared/Infrastructure/Doctrine/MoocEntityManagerFactory.php: -------------------------------------------------------------------------------- 1 | questions = $questions; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Mooc/Steps/Domain/Quiz/QuizStepQuestion.php: -------------------------------------------------------------------------------- 1 | question . '----' . implode('****', $this->answers); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Mooc/Steps/Domain/Step.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Mooc/Steps/Infrastructure/Persistence/Doctrine/Exercise.ExerciseStepContent.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Mooc/Steps/Infrastructure/Persistence/Doctrine/Quiz.QuizStep.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Mooc/Steps/Infrastructure/Persistence/Doctrine/StepDuration.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Mooc/Steps/Infrastructure/Persistence/Doctrine/StepIdType.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Mooc/Steps/Infrastructure/Persistence/Doctrine/Video.VideoStep.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Mooc/Steps/Infrastructure/Persistence/Doctrine/Video.VideoStepUrl.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Mooc/Steps/Infrastructure/Persistence/MySqlStepRepository.php: -------------------------------------------------------------------------------- 1 | persist($step); 17 | } 18 | 19 | public function search(StepId $id): ?Step 20 | { 21 | return $this->repository(Step::class)->find($id); 22 | } 23 | 24 | public function delete(Step $step): void 25 | { 26 | $this->remove($step); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Application/Create/CreateVideoCommand.php: -------------------------------------------------------------------------------- 1 | id; 22 | } 23 | 24 | public function type(): string 25 | { 26 | return $this->type; 27 | } 28 | 29 | public function title(): string 30 | { 31 | return $this->title; 32 | } 33 | 34 | public function url(): string 35 | { 36 | return $this->url; 37 | } 38 | 39 | public function courseId(): string 40 | { 41 | return $this->courseId; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Application/Create/CreateVideoCommandHandler.php: -------------------------------------------------------------------------------- 1 | id()); 21 | $type = VideoType::from($command->type()); 22 | $title = new VideoTitle($command->title()); 23 | $url = new VideoUrl($command->url()); 24 | $courseId = new CourseId($command->courseId()); 25 | 26 | $this->creator->create($id, $type, $title, $url, $courseId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Application/Create/VideoCreator.php: -------------------------------------------------------------------------------- 1 | repository->save($video); 25 | 26 | $this->bus->publish(...$video->pullDomainEvents()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Application/Find/FindVideoQuery.php: -------------------------------------------------------------------------------- 1 | id; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Application/Find/FindVideoQueryHandler.php: -------------------------------------------------------------------------------- 1 | responseConverter = new VideoResponseConverter(); 19 | } 20 | 21 | public function __invoke(FindVideoQuery $query): VideoResponse 22 | { 23 | $id = new VideoId($query->id()); 24 | 25 | $video = apply($this->finder, [$id]); 26 | 27 | return apply($this->responseConverter, [$video]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Application/Find/VideoFinder.php: -------------------------------------------------------------------------------- 1 | finder = new DomainVideoFinder($repository); 19 | } 20 | 21 | public function __invoke(VideoId $id): Video 22 | { 23 | return $this->finder->__invoke($id); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Application/Find/VideoResponse.php: -------------------------------------------------------------------------------- 1 | id()->value(), 15 | $video->type()->value, 16 | $video->title()->value(), 17 | $video->url()->value(), 18 | $video->courseId()->value() 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Application/Trim/TrimVideoCommand.php: -------------------------------------------------------------------------------- 1 | videoId; 16 | } 17 | 18 | public function keepFromSecond(): int 19 | { 20 | return $this->keepFromSecond; 21 | } 22 | 23 | public function keepToSecond(): int 24 | { 25 | return $this->keepToSecond; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Application/Trim/TrimVideoCommandHandler.php: -------------------------------------------------------------------------------- 1 | videoId()); 17 | $interval = SecondsInterval::fromValues($command->keepFromSecond(), $command->keepToSecond()); 18 | 19 | $this->trimmer->trim($id, $interval); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Application/Trim/VideoTrimmer.php: -------------------------------------------------------------------------------- 1 | finder = new VideoFinder($repository); 19 | } 20 | 21 | public function __invoke(VideoId $id, VideoTitle $newTitle): void 22 | { 23 | $video = $this->finder->__invoke($id); 24 | 25 | $video->updateTitle($newTitle); 26 | 27 | $this->repository->save($video); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Domain/VideoCreatedDomainEvent.php: -------------------------------------------------------------------------------- 1 | $this->type, 49 | 'title' => $this->title, 50 | 'url' => $this->url, 51 | 'course_id' => $this->courseId, 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Domain/VideoFinder.php: -------------------------------------------------------------------------------- 1 | repository->search($id); 14 | 15 | if ($video === null) { 16 | throw new VideoNotFound($id); 17 | } 18 | 19 | return $video; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Domain/VideoId.php: -------------------------------------------------------------------------------- 1 | has not been found', $this->id->value()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Domain/VideoRepository.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Infrastructure/Persistence/Doctrine/VideoIdType.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Mooc/Videos/Infrastructure/Persistence/Doctrine/VideoType.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Retention/Campaign/Application/NewCourseAvailable/Schedule/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Campaign/Application/NewCourseAvailable/Schedule/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Campaign/Application/NewCourseAvailable/Trigger/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Campaign/Application/NewCourseAvailable/Trigger/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Campaign/Application/WelcomeUser/Trigger/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Campaign/Application/WelcomeUser/Trigger/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Campaign/Domain/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Campaign/Domain/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Campaign/Infrastructure/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Campaign/Infrastructure/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Email/Application/SendNewCourseAvailable/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Email/Application/SendNewCourseAvailable/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Email/Application/SendWelcomeUser/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Email/Application/SendWelcomeUser/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Email/Domain/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Email/Domain/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Email/Infrastructure/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Email/Infrastructure/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Push/Application/SendNewCourseAvailable/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Push/Application/SendNewCourseAvailable/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Push/Domain/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Push/Domain/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Push/Infrastructure/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Push/Infrastructure/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Sms/Application/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Sms/Application/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Sms/Domain/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Sms/Domain/.gitkeep -------------------------------------------------------------------------------- /src/Retention/Sms/Infrastructure/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/src/Retention/Sms/Infrastructure/.gitkeep -------------------------------------------------------------------------------- /src/Shared/Domain/Aggregate/AggregateRoot.php: -------------------------------------------------------------------------------- 1 | domainEvents; 16 | $this->domainEvents = []; 17 | 18 | return $domainEvents; 19 | } 20 | 21 | final protected function record(DomainEvent $domainEvent): void 22 | { 23 | $this->domainEvents[] = $domainEvent; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Shared/Domain/Assert.php: -------------------------------------------------------------------------------- 1 | is not an instance of <%s>', $class, $item::class)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Shared/Domain/Bus/Command/Command.php: -------------------------------------------------------------------------------- 1 | eventId = $eventId ?: SimpleUuid::random()->value(); 19 | $this->occurredOn = $occurredOn ?: Utils::dateToString(new DateTimeImmutable()); 20 | } 21 | 22 | abstract public static function fromPrimitives( 23 | string $aggregateId, 24 | array $body, 25 | string $eventId, 26 | string $occurredOn 27 | ): self; 28 | 29 | abstract public static function eventName(): string; 30 | 31 | abstract public function toPrimitives(): array; 32 | 33 | final public function aggregateId(): string 34 | { 35 | return $this->aggregateId; 36 | } 37 | 38 | final public function eventId(): string 39 | { 40 | return $this->eventId; 41 | } 42 | 43 | final public function occurredOn(): string 44 | { 45 | return $this->occurredOn; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Shared/Domain/Bus/Event/DomainEventSubscriber.php: -------------------------------------------------------------------------------- 1 | */ 13 | abstract class Collection implements Countable, IteratorAggregate 14 | { 15 | public function __construct(private readonly array $items) 16 | { 17 | Assert::arrayOf($this->type(), $items); 18 | } 19 | 20 | abstract protected function type(): string; 21 | 22 | final public function getIterator(): Traversable 23 | { 24 | return new ArrayIterator($this->items()); 25 | } 26 | 27 | final public function count(): int 28 | { 29 | return count($this->items()); 30 | } 31 | 32 | protected function items(): array 33 | { 34 | return $this->items; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Shared/Domain/Criteria/Criteria.php: -------------------------------------------------------------------------------- 1 | filters->count() > 0; 19 | } 20 | 21 | public function hasOrder(): bool 22 | { 23 | return !$this->order->isNone(); 24 | } 25 | 26 | public function plainFilters(): array 27 | { 28 | return $this->filters->filters(); 29 | } 30 | 31 | public function filters(): Filters 32 | { 33 | return $this->filters; 34 | } 35 | 36 | public function order(): Order 37 | { 38 | return $this->order; 39 | } 40 | 41 | public function offset(): ?int 42 | { 43 | return $this->offset; 44 | } 45 | 46 | public function limit(): ?int 47 | { 48 | return $this->limit; 49 | } 50 | 51 | public function serialize(): string 52 | { 53 | return sprintf( 54 | '%s~~%s~~%s~~%s', 55 | $this->filters->serialize(), 56 | $this->order->serialize(), 57 | $this->offset ?? 'none', 58 | $this->limit ?? 'none' 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Shared/Domain/Criteria/Filter.php: -------------------------------------------------------------------------------- 1 | field; 27 | } 28 | 29 | public function operator(): FilterOperator 30 | { 31 | return $this->operator; 32 | } 33 | 34 | public function value(): FilterValue 35 | { 36 | return $this->value; 37 | } 38 | 39 | public function serialize(): string 40 | { 41 | return sprintf('%s.%s.%s', $this->field->value(), $this->operator->value, $this->value->value()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Shared/Domain/Criteria/FilterField.php: -------------------------------------------------------------------------------- 1 | '; 12 | case LT = '<'; 13 | case CONTAINS = 'CONTAINS'; 14 | case NOT_CONTAINS = 'NOT_CONTAINS'; 15 | 16 | public function isContaining(): bool 17 | { 18 | return in_array($this->value, [self::CONTAINS->value, self::NOT_CONTAINS->value], true); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Shared/Domain/Criteria/FilterValue.php: -------------------------------------------------------------------------------- 1 | Filter::fromValues($values); 21 | } 22 | 23 | public function add(Filter $filter): self 24 | { 25 | return new self(array_merge($this->items(), [$filter])); 26 | } 27 | 28 | public function filters(): array 29 | { 30 | return $this->items(); 31 | } 32 | 33 | public function serialize(): string 34 | { 35 | return reduce( 36 | static fn (string $accumulate, Filter $filter): string => sprintf('%s^%s', $accumulate, $filter->serialize()), 37 | $this->items(), 38 | '' 39 | ); 40 | } 41 | 42 | protected function type(): string 43 | { 44 | return Filter::class; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Shared/Domain/Criteria/Order.php: -------------------------------------------------------------------------------- 1 | orderBy; 32 | } 33 | 34 | public function orderType(): OrderType 35 | { 36 | return $this->orderType; 37 | } 38 | 39 | public function isNone(): bool 40 | { 41 | return $this->orderType()->isNone(); 42 | } 43 | 44 | public function serialize(): string 45 | { 46 | return sprintf('%s.%s', $this->orderBy->value(), $this->orderType->value); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Shared/Domain/Criteria/OrderBy.php: -------------------------------------------------------------------------------- 1 | value === self::NONE->value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Shared/Domain/DomainError.php: -------------------------------------------------------------------------------- 1 | errorMessage()); 14 | } 15 | 16 | abstract public function errorCode(): string; 17 | 18 | abstract protected function errorMessage(): string; 19 | } 20 | -------------------------------------------------------------------------------- /src/Shared/Domain/Logger.php: -------------------------------------------------------------------------------- 1 | ensureIntervalEndsAfterStart($from, $to); 14 | } 15 | 16 | public static function fromValues(int $from, int $to): self 17 | { 18 | return new self(new Second($from), new Second($to)); 19 | } 20 | 21 | private function ensureIntervalEndsAfterStart(Second $from, Second $to): void 22 | { 23 | if ($from->isBiggerThan($to)) { 24 | throw new DomainException('To is bigger than from'); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Shared/Domain/UuidGenerator.php: -------------------------------------------------------------------------------- 1 | value; 14 | } 15 | 16 | final public function isBiggerThan(self $other): bool 17 | { 18 | return $this->value() > $other->value(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Shared/Domain/ValueObject/SimpleUuid.php: -------------------------------------------------------------------------------- 1 | value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Shared/Domain/ValueObject/Uuid.php: -------------------------------------------------------------------------------- 1 | ensureIsValidUuid($value); 16 | } 17 | 18 | final public static function random(): self 19 | { 20 | return new static(RamseyUuid::uuid4()->toString()); 21 | } 22 | 23 | final public function value(): string 24 | { 25 | return $this->value; 26 | } 27 | 28 | final public function equals(self $other): bool 29 | { 30 | return $this->value() === $other->value(); 31 | } 32 | 33 | public function __toString(): string 34 | { 35 | return $this->value(); 36 | } 37 | 38 | private function ensureIsValidUuid(string $id): void 39 | { 40 | if (!RamseyUuid::isValid($id)) { 41 | throw new InvalidArgumentException(sprintf('<%s> does not allow the value <%s>.', self::class, $id)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Bus/Command/CommandNotRegisteredError.php: -------------------------------------------------------------------------------- 1 | hasn't a command handler associated"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Bus/Event/DomainEventJsonDeserializer.php: -------------------------------------------------------------------------------- 1 | mapping->for($eventName); 19 | 20 | return $eventClass::fromPrimitives( 21 | $eventData['data']['attributes']['id'], 22 | $eventData['data']['attributes'], 23 | $eventData['data']['id'], 24 | $eventData['data']['occurred_on'] 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Bus/Event/DomainEventJsonSerializer.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'id' => $domainEvent->eventId(), 17 | 'type' => $domainEvent::eventName(), 18 | 'occurred_on' => $domainEvent->occurredOn(), 19 | 'attributes' => array_merge($domainEvent->toPrimitives(), ['id' => $domainEvent->aggregateId()]), 20 | ], 21 | 'meta' => [], 22 | ] 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Bus/Event/DomainEventMapping.php: -------------------------------------------------------------------------------- 1 | mapping = reduce($this->eventsExtractor(), $mapping, []); 20 | } 21 | 22 | public function for(string $name): string 23 | { 24 | if (!isset($this->mapping[$name])) { 25 | throw new RuntimeException("The Domain Event Class for <$name> doesn't exists or have no subscribers"); 26 | } 27 | 28 | return $this->mapping[$name]; 29 | } 30 | 31 | private function eventsExtractor(): callable 32 | { 33 | return fn (array $mapping, DomainEventSubscriber $subscriber): array => array_merge( 34 | $mapping, 35 | reindex($this->eventNameExtractor(), $subscriber::subscribedTo()) 36 | ); 37 | } 38 | 39 | private function eventNameExtractor(): callable 40 | { 41 | return static fn (string $eventClass): string => $eventClass::eventName(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Bus/Event/InMemory/InMemorySymfonyEventBus.php: -------------------------------------------------------------------------------- 1 | bus = new MessageBus( 22 | [ 23 | new HandleMessageMiddleware( 24 | new HandlersLocator(CallableFirstParameterExtractor::forPipedCallables($subscribers)) 25 | ), 26 | ] 27 | ); 28 | } 29 | 30 | public function publish(DomainEvent ...$events): void 31 | { 32 | foreach ($events as $event) { 33 | try { 34 | $this->bus->dispatch($event); 35 | } catch (NoHandlerForMessageException) { 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Bus/Event/RabbitMq/RabbitMqExchangeNameFormatter.php: -------------------------------------------------------------------------------- 1 | monitor->registry()->getOrRegisterCounter( 24 | $this->appName, 25 | 'domain_event', 26 | 'Domain Events', 27 | ['name'] 28 | ); 29 | 30 | each(fn (DomainEvent $event) => $counter->inc(['name' => $event::eventName()]), $events); 31 | 32 | $this->bus->publish(...$events); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Bus/Query/QueryNotRegisteredError.php: -------------------------------------------------------------------------------- 1 | has no associated query handler"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Cdc/DatabaseMutationAction.php: -------------------------------------------------------------------------------- 1 | connections = Utils::iterableToArray($connections); 21 | } 22 | 23 | public function clear(): void 24 | { 25 | each(fn (EntityManager $entityManager) => $entityManager->clear(), $this->connections); 26 | } 27 | 28 | public function truncate(): void 29 | { 30 | apply(new MySqlDatabaseCleaner(), array_values($this->connections)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Doctrine/Dbal/DbalCustomTypesRegistrar.php: -------------------------------------------------------------------------------- 1 | client->index( 16 | [ 17 | 'index' => sprintf('%s_%s', $this->indexPrefix, $aggregateName), 18 | 'id' => $identifier, 19 | 'body' => $plainBody, 20 | ] 21 | ); 22 | } 23 | 24 | public function client(): Client 25 | { 26 | return $this->client; 27 | } 28 | 29 | public function indexPrefix(): string 30 | { 31 | return $this->indexPrefix; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Logger/MonologLogger.php: -------------------------------------------------------------------------------- 1 | logger->info($message, $context); 16 | } 17 | 18 | public function warning(string $message, array $context = []): void 19 | { 20 | $this->logger->warning($message, $context); 21 | } 22 | 23 | public function critical(string $message, array $context = []): void 24 | { 25 | $this->logger->critical($message, $context); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Monitoring/PrometheusMonitor.php: -------------------------------------------------------------------------------- 1 | registry = new CollectorRegistry(new APC()); 17 | } 18 | 19 | public function registry(): CollectorRegistry 20 | { 21 | return $this->registry; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Persistence/Doctrine/UuidType.php: -------------------------------------------------------------------------------- 1 | typeClassName(); 32 | 33 | return new $className($value); 34 | } 35 | 36 | final public function convertToDatabaseValue($value, AbstractPlatform $platform): string 37 | { 38 | /** @var Uuid $value */ 39 | return $value->value(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/PhpRandomNumberGenerator.php: -------------------------------------------------------------------------------- 1 | toString(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Symfony/ApiController.php: -------------------------------------------------------------------------------- 1 | $exceptionHandler->register($exceptionClass, $httpCode), 24 | $this->exceptions() 25 | ); 26 | } 27 | 28 | abstract protected function exceptions(): array; 29 | 30 | protected function ask(Query $query): ?Response 31 | { 32 | return $this->queryBus->ask($query); 33 | } 34 | 35 | protected function dispatch(Command $command): void 36 | { 37 | $this->commandBus->dispatch($command); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Symfony/ApiExceptionsHttpStatusCodeMapping.php: -------------------------------------------------------------------------------- 1 | Response::HTTP_BAD_REQUEST, 18 | NotFoundHttpException::class => Response::HTTP_NOT_FOUND, 19 | ]; 20 | 21 | public function register(string $exceptionClass, int $statusCode): void 22 | { 23 | $this->exceptions[$exceptionClass] = $statusCode; 24 | } 25 | 26 | public function statusCodeFor(string $exceptionClass): int 27 | { 28 | $statusCode = get($exceptionClass, $this->exceptions, self::DEFAULT_STATUS_CODE); 29 | 30 | if ($statusCode === null) { 31 | throw new InvalidArgumentException("There are no status code mapping for <$exceptionClass>"); 32 | } 33 | 34 | return $statusCode; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Shared/Infrastructure/Symfony/FlashSession.php: -------------------------------------------------------------------------------- 1 | getSession()->getFlashBag()->all()); 17 | } 18 | 19 | public function get(string $key, $default = null) 20 | { 21 | if (array_key_exists($key, self::$flashes)) { 22 | return self::$flashes[$key]; 23 | } 24 | 25 | if (array_key_exists($key . '.0', self::$flashes)) { 26 | return self::$flashes[$key . '.0']; 27 | } 28 | 29 | if (array_key_exists($key . '.0.0', self::$flashes)) { 30 | return self::$flashes[$key . '.0.0']; 31 | } 32 | 33 | return $default; 34 | } 35 | 36 | public function has(string $key): bool 37 | { 38 | return array_key_exists($key, self::$flashes) 39 | || array_key_exists($key . '.0', self::$flashes) 40 | || array_key_exists($key . '.0.0', self::$flashes); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Backoffice/Auth/Application/Authenticate/AuthenticateUserCommandMother.php: -------------------------------------------------------------------------------- 1 | value() ?? AuthUsernameMother::create()->value(), 21 | $password?->value() ?? AuthPasswordMother::create()->value() 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Backoffice/Auth/AuthModuleUnitTestCase.php: -------------------------------------------------------------------------------- 1 | repository() 20 | ->shouldReceive('search') 21 | ->with($this->similarTo($username)) 22 | ->once() 23 | ->andReturn($authUser); 24 | } 25 | 26 | protected function repository(): AuthRepository | MockInterface 27 | { 28 | return $this->repository ??= $this->mock(AuthRepository::class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Backoffice/Auth/Domain/AuthPasswordMother.php: -------------------------------------------------------------------------------- 1 | username()), 23 | AuthPasswordMother::create($command->password()) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Backoffice/Auth/Domain/AuthUsernameMother.php: -------------------------------------------------------------------------------- 1 | service(EntityManager::class)); 17 | } 18 | 19 | protected function elasticRepository(): ElasticsearchBackofficeCourseRepository 20 | { 21 | return $this->service(ElasticsearchBackofficeCourseRepository::class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Backoffice/Courses/Domain/BackofficeCourseCriteriaMother.php: -------------------------------------------------------------------------------- 1 | 'name', 20 | 'operator' => 'CONTAINS', 21 | 'value' => $text, 22 | ]) 23 | ) 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Backoffice/Courses/Domain/BackofficeCourseMother.php: -------------------------------------------------------------------------------- 1 | value(), 18 | $name ?? CourseNameMother::create()->value(), 19 | $duration ?? CourseDurationMother::create()->value() 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Backoffice/Shared/Infraestructure/PhpUnit/BackofficeContextInfrastructureTestCase.php: -------------------------------------------------------------------------------- 1 | service(ElasticsearchClient::class), 20 | $this->service(EntityManager::class) 21 | ); 22 | 23 | $arranger->arrange(); 24 | } 25 | 26 | protected function kernelClass(): string 27 | { 28 | return BackofficeBackendKernel::class; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Backoffice/Shared/Infraestructure/PhpUnit/BackofficeEnvironmentArranger.php: -------------------------------------------------------------------------------- 1 | elasticsearchClient]); 22 | apply(new MySqlDatabaseCleaner(), [$this->entityManager]); 23 | } 24 | 25 | public function close(): void {} 26 | } 27 | -------------------------------------------------------------------------------- /tests/Mooc/Courses/Application/Create/CreateCourseCommandMother.php: -------------------------------------------------------------------------------- 1 | value() ?? CourseIdMother::create()->value(), 24 | $name?->value() ?? CourseNameMother::create()->value(), 25 | $duration?->value() ?? CourseDurationMother::create()->value() 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Mooc/Courses/CoursesModuleInfrastructureTestCase.php: -------------------------------------------------------------------------------- 1 | service(CourseRepository::class); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Mooc/Courses/CoursesModuleUnitTestCase.php: -------------------------------------------------------------------------------- 1 | repository() 20 | ->shouldReceive('save') 21 | ->with($this->similarTo($course)) 22 | ->once() 23 | ->andReturnNull(); 24 | } 25 | 26 | protected function shouldSearch(CourseId $id, ?Course $course): void 27 | { 28 | $this->repository() 29 | ->shouldReceive('search') 30 | ->with($this->similarTo($id)) 31 | ->once() 32 | ->andReturn($course); 33 | } 34 | 35 | protected function repository(): CourseRepository | MockInterface 36 | { 37 | return $this->repository ??= $this->mock(CourseRepository::class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Mooc/Courses/Domain/CourseCreatedDomainEventMother.php: -------------------------------------------------------------------------------- 1 | value() ?? CourseIdMother::create()->value(), 22 | $name?->value() ?? CourseNameMother::create()->value(), 23 | $duration?->value() ?? CourseDurationMother::create()->value() 24 | ); 25 | } 26 | 27 | public static function fromCourse(Course $course): CourseCreatedDomainEvent 28 | { 29 | return self::create($course->id(), $course->name(), $course->duration()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Mooc/Courses/Domain/CourseDurationMother.php: -------------------------------------------------------------------------------- 1 | id()), 31 | CourseNameMother::create($request->name()), 32 | CourseDurationMother::create($request->duration()) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Mooc/Courses/Domain/CourseNameMother.php: -------------------------------------------------------------------------------- 1 | repository()->save($course); 19 | } 20 | 21 | /** @test */ 22 | public function it_should_return_an_existing_course(): void 23 | { 24 | $course = CourseMother::create(); 25 | 26 | $this->repository()->save($course); 27 | 28 | $this->assertEquals($course, $this->repository()->search($course->id())); 29 | } 30 | 31 | /** @test */ 32 | public function it_should_not_return_a_non_existing_course(): void 33 | { 34 | $this->assertNull($this->repository()->search(CourseIdMother::create())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Mooc/CoursesCounter/Application/Find/CoursesCounterResponseMother.php: -------------------------------------------------------------------------------- 1 | value() ?? CoursesCounterTotalMother::create()->value()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Mooc/CoursesCounter/CoursesCounterModuleUnitTestCase.php: -------------------------------------------------------------------------------- 1 | repository() 19 | ->shouldReceive('save') 20 | ->once() 21 | ->with($this->similarTo($course)) 22 | ->andReturnNull(); 23 | } 24 | 25 | protected function shouldSearch(?CoursesCounter $counter): void 26 | { 27 | $this->repository() 28 | ->shouldReceive('search') 29 | ->once() 30 | ->andReturn($counter); 31 | } 32 | 33 | protected function repository(): CoursesCounterRepository | MockInterface 34 | { 35 | return $this->repository ??= $this->mock(CoursesCounterRepository::class); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Mooc/CoursesCounter/Domain/CoursesCounterIdMother.php: -------------------------------------------------------------------------------- 1 | value() ?? CoursesCounterIdMother::create()->value(), 20 | $total?->value() ?? CoursesCounterTotalMother::create()->value() 21 | ); 22 | } 23 | 24 | public static function fromCounter(CoursesCounter $counter): CoursesCounterIncrementedDomainEvent 25 | { 26 | return self::create($counter->id(), $counter->total()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Mooc/CoursesCounter/Domain/CoursesCounterTotalMother.php: -------------------------------------------------------------------------------- 1 | service(EntityManager::class)); 18 | 19 | $arranger->arrange(); 20 | } 21 | 22 | protected function tearDown(): void 23 | { 24 | $arranger = new MoocEnvironmentArranger($this->service(EntityManager::class)); 25 | 26 | $arranger->close(); 27 | 28 | parent::tearDown(); 29 | } 30 | 31 | protected function kernelClass(): string 32 | { 33 | return MoocBackendKernel::class; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Mooc/Shared/Infrastructure/PhpUnit/MoocEnvironmentArranger.php: -------------------------------------------------------------------------------- 1 | entityManager]); 20 | } 21 | 22 | public function close(): void {} 23 | } 24 | -------------------------------------------------------------------------------- /tests/Mooc/Steps/Domain/Exercise/ExerciseStepContentMother.php: -------------------------------------------------------------------------------- 1 | QuizStepQuestionMother::create() 27 | ) : $questions; 28 | 29 | return new QuizStep( 30 | $id ?? StepIdMother::create(), 31 | $title ?? StepTitleMother::create(), 32 | $duration ?? StepDurationMother::create(), 33 | ...$stepQuestions 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Mooc/Steps/Domain/Quiz/QuizStepQuestionMother.php: -------------------------------------------------------------------------------- 1 | WordMother::create()) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Mooc/Steps/Domain/StepDurationMother.php: -------------------------------------------------------------------------------- 1 | service(StepRepository::class); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Mooc/Videos/Application/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/tests/Mooc/Videos/Application/.gitkeep -------------------------------------------------------------------------------- /tests/Mooc/Videos/Domain/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/tests/Mooc/Videos/Domain/.gitkeep -------------------------------------------------------------------------------- /tests/Mooc/Videos/Infrastructure/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodelyTV/php-ddd-example/9271c467943f835ab3b6e5ba30bdbd710aca9d73/tests/Mooc/Videos/Infrastructure/.gitkeep -------------------------------------------------------------------------------- /tests/Shared/Domain/Criteria/CriteriaMother.php: -------------------------------------------------------------------------------- 1 | getName()])) { 22 | $property->setAccessible(true); 23 | $property->setValue($duplicated, $newParams[$property->getName()]); 24 | } 25 | }, 26 | $reflection->getProperties() 27 | ); 28 | 29 | return $duplicated; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Shared/Domain/IntegerMother.php: -------------------------------------------------------------------------------- 1 | numberBetween($min, $max); 17 | } 18 | 19 | public static function lessThan(int $max): int 20 | { 21 | return self::between(1, $max); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Shared/Domain/MotherCreator.php: -------------------------------------------------------------------------------- 1 | randomElement($elements); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Shared/Domain/Repeater.php: -------------------------------------------------------------------------------- 1 | evaluate($actual, '', true); 17 | } 18 | 19 | public static function assertSimilar(mixed $expected, mixed $actual): void 20 | { 21 | $constraint = new CodelyTvConstraintIsSimilar($expected); 22 | 23 | $constraint->evaluate($actual); 24 | } 25 | 26 | public static function similarTo(mixed $value, float $delta = 0.0): CodelyTvMatcherIsSimilar 27 | { 28 | return new CodelyTvMatcherIsSimilar($value, $delta); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Shared/Domain/UuidMother.php: -------------------------------------------------------------------------------- 1 | unique()->uuid; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Shared/Domain/WordMother.php: -------------------------------------------------------------------------------- 1 | word; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Shared/Infrastructure/ArchitectureTest.php: -------------------------------------------------------------------------------- 1 | connections->clear(); 25 | $this->connections->truncate(); 26 | } 27 | 28 | /** 29 | * @Given /^I send an event to the event bus:$/ 30 | */ 31 | public function iSendAnEventToTheEventBus(PyStringNode $event): void 32 | { 33 | $domainEvent = $this->deserializer->deserialize($event->getRaw()); 34 | 35 | $this->bus->publish($domainEvent); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Shared/Infrastructure/Bus/Command/FakeCommand.php: -------------------------------------------------------------------------------- 1 | number; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Shared/Infrastructure/ConstantRandomNumberGenerator.php: -------------------------------------------------------------------------------- 1 | client()->cat()->indices(); 16 | 17 | each( 18 | static function (array $index) use ($client): void { 19 | $indexName = $index['index']; 20 | 21 | $client->client()->indices()->delete(['index' => $indexName]); 22 | $client->client()->indices()->create(['index' => $indexName]); 23 | }, 24 | $indices 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Shared/Infrastructure/Mink/MinkSessionRequestHelper.php: -------------------------------------------------------------------------------- 1 | request($method, $url, $optionalParams); 17 | } 18 | 19 | public function sendRequestWithPyStringNode($method, $url, PyStringNode $body): void 20 | { 21 | $this->request($method, $url, ['content' => $body->getRaw()]); 22 | } 23 | 24 | public function request(string $method, string $url, array $optionalParams = []): Crawler 25 | { 26 | return $this->sessionHelper->sendRequest($method, $url, $optionalParams); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Shared/Infrastructure/Mockery/CodelyTvMatcherIsSimilar.php: -------------------------------------------------------------------------------- 1 | constraint = new CodelyTvConstraintIsSimilar($value, $delta); 18 | } 19 | 20 | public function match(&$actual): bool 21 | { 22 | return $this->constraint->evaluate($actual, '', true); 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return 'Is similar'; 28 | } 29 | } 30 | --------------------------------------------------------------------------------