├── .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 |

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 |
--------------------------------------------------------------------------------