├── phpstan.neon.dist ├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── App ├── Application │ ├── .gitkeep │ ├── Events │ │ └── EventDispatcher.php │ └── Members │ │ ├── SendMemberActivationEmail.php │ │ ├── SignUpMember.php │ │ └── SignUpMemberCommandHandler.php ├── Domain │ ├── .gitkeep │ └── Members │ │ ├── EmailAddress.php │ │ ├── EmailAddressIsAlreadyTaken.php │ │ ├── FirstName.php │ │ ├── Id.php │ │ ├── LastName.php │ │ ├── Member.php │ │ ├── MemberReadModel.php │ │ ├── MemberRepository.php │ │ ├── MemberSignedUp.php │ │ ├── MemberWasNotFound.php │ │ ├── MemberWasVerified.php │ │ ├── Password.php │ │ └── StatusName.php └── Infrastructure │ ├── .gitkeep │ ├── Authentication │ ├── User.php │ └── UserFactory.php │ ├── Home │ └── HomeController.php │ ├── Laravel │ ├── Bootstrap │ │ ├── app.php │ │ └── cache │ │ │ └── .gitignore │ ├── Config │ │ ├── app.php │ │ ├── auth.php │ │ ├── broadcasting.php │ │ ├── cache.php │ │ ├── cors.php │ │ ├── database.php │ │ ├── filesystems.php │ │ ├── flare.php │ │ ├── hashing.php │ │ ├── ignition.php │ │ ├── logging.php │ │ ├── mail.php │ │ ├── members.php │ │ ├── queue.php │ │ ├── route-attributes.php │ │ ├── sanctum.php │ │ ├── services.php │ │ ├── session.php │ │ ├── snowflake.php │ │ ├── tinker.php │ │ └── view.php │ ├── Console │ │ └── Kernel.php │ ├── Database │ │ ├── .gitignore │ │ ├── Migrations │ │ │ ├── 2014_10_12_000000_create_users_table.php │ │ │ ├── 2014_10_12_100000_create_password_resets_table.php │ │ │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ │ │ └── 2019_12_14_000001_create_personal_access_tokens_table.php │ │ └── Seeders │ │ │ └── DatabaseSeeder.php │ ├── Events │ │ ├── Dispatcher.php │ │ ├── FakeEventDispatcher.php │ │ └── NullDispatcher.php │ ├── Exceptions │ │ └── Handler.php │ ├── Foundation │ │ └── Application.php │ ├── Http │ │ ├── Kernel.php │ │ └── Middleware │ │ │ ├── Authenticate.php │ │ │ ├── EncryptCookies.php │ │ │ ├── PreventRequestsDuringMaintenance.php │ │ │ ├── TrimStrings.php │ │ │ ├── TrustHosts.php │ │ │ ├── TrustProxies.php │ │ │ ├── ValidateSignature.php │ │ │ └── VerifyCsrfToken.php │ ├── Language │ │ └── en │ │ │ ├── auth.php │ │ │ ├── pagination.php │ │ │ ├── passwords.php │ │ │ └── validation.php │ ├── Models │ │ └── BaseModel.php │ ├── Providers │ │ ├── AuthServiceProvider.php │ │ ├── BroadcastServiceProvider.php │ │ ├── BusServiceProvider.php │ │ ├── ClockServiceProvider.php │ │ ├── EventServiceProvider.php │ │ ├── MemberServiceProvider.php │ │ ├── ModelFactoryServiceProvider.php │ │ ├── ModelServiceProvider.php │ │ ├── RelationServiceProvider.php │ │ ├── RouteAttributesServiceProvider.php │ │ └── RouteServiceProvider.php │ ├── Resources │ │ └── Views │ │ │ ├── errors │ │ │ ├── 401.blade.php │ │ │ ├── 403.blade.php │ │ │ ├── 404.blade.php │ │ │ ├── 419.blade.php │ │ │ ├── 429.blade.php │ │ │ ├── 500.blade.php │ │ │ ├── 503.blade.php │ │ │ ├── layout.blade.php │ │ │ └── minimal.blade.php │ │ │ └── vendor │ │ │ ├── mail │ │ │ ├── html │ │ │ │ ├── button.blade.php │ │ │ │ ├── footer.blade.php │ │ │ │ ├── header.blade.php │ │ │ │ ├── layout.blade.php │ │ │ │ ├── message.blade.php │ │ │ │ ├── panel.blade.php │ │ │ │ ├── subcopy.blade.php │ │ │ │ ├── table.blade.php │ │ │ │ └── themes │ │ │ │ │ └── default.css │ │ │ └── text │ │ │ │ ├── button.blade.php │ │ │ │ ├── footer.blade.php │ │ │ │ ├── header.blade.php │ │ │ │ ├── layout.blade.php │ │ │ │ ├── message.blade.php │ │ │ │ ├── panel.blade.php │ │ │ │ ├── subcopy.blade.php │ │ │ │ └── table.blade.php │ │ │ └── notifications │ │ │ └── email.blade.php │ ├── Storage │ │ ├── Logs │ │ │ └── .gitignore │ │ ├── app │ │ │ ├── .gitignore │ │ │ └── public │ │ │ │ └── .gitignore │ │ └── framework │ │ │ ├── .gitignore │ │ │ ├── cache │ │ │ ├── .gitignore │ │ │ └── data │ │ │ │ └── .gitignore │ │ │ ├── sessions │ │ │ ├── .gitignore │ │ │ └── uHxTGZFnEW8wUYP2fcXhJLrzjWzmY8hFdX6EYoSF │ │ │ ├── testing │ │ │ └── .gitignore │ │ │ └── views │ │ │ └── .gitignore │ ├── Tests │ │ ├── Application │ │ │ └── Members │ │ │ │ └── SignUpMemberCommandHandlerTest.php │ │ ├── CreatesApplication.php │ │ ├── Domain │ │ │ └── Members │ │ │ │ └── MemberTest.php │ │ ├── Infrastructure │ │ │ └── Members │ │ │ │ ├── EloquentMemberRepositoryTest.php │ │ │ │ └── SignUpMemberControllerTest.php │ │ └── TestCase.php │ └── stubs │ │ ├── cast.inbound.stub │ │ ├── cast.stub │ │ ├── console.stub │ │ ├── controller.api.stub │ │ ├── controller.invokable.stub │ │ ├── controller.model.api.stub │ │ ├── controller.model.stub │ │ ├── controller.nested.api.stub │ │ ├── controller.nested.stub │ │ ├── controller.plain.stub │ │ ├── controller.stub │ │ ├── event.stub │ │ ├── factory.stub │ │ ├── job.queued.stub │ │ ├── job.stub │ │ ├── mail.stub │ │ ├── markdown-mail.stub │ │ ├── markdown-notification.stub │ │ ├── middleware.stub │ │ ├── migration.create.stub │ │ ├── migration.stub │ │ ├── migration.update.stub │ │ ├── model.pivot.stub │ │ ├── model.stub │ │ ├── notification.stub │ │ ├── observer.plain.stub │ │ ├── observer.stub │ │ ├── policy.plain.stub │ │ ├── policy.stub │ │ ├── provider.stub │ │ ├── request.stub │ │ ├── resource-collection.stub │ │ ├── resource.stub │ │ ├── rule.stub │ │ ├── scope.stub │ │ ├── seeder.stub │ │ ├── test.stub │ │ ├── test.unit.stub │ │ └── view-component.stub │ └── Members │ ├── ArrayMemberRepository.php │ ├── EloquentMemberRepository.php │ ├── Member.php │ ├── MemberActivationEmail.php │ ├── MemberFactory.php │ ├── SignUpMemberController.php │ ├── SignUpMemberFormRequest.php │ └── activation.blade.php ├── LICENSE ├── README.md ├── _ide_helper.php ├── artisan ├── behat.yaml ├── composer.json ├── composer.lock ├── deptrac.yaml ├── docker-compose.yml ├── docker ├── 7.4 │ ├── Dockerfile │ ├── php.ini │ ├── start-container │ └── supervisord.conf ├── 8.0 │ ├── Dockerfile │ ├── php.ini │ ├── start-container │ └── supervisord.conf ├── 8.1 │ ├── Dockerfile │ ├── php.ini │ ├── start-container │ └── supervisord.conf └── 8.2 │ ├── Dockerfile │ ├── php.ini │ ├── start-container │ └── supervisord.conf ├── infection.json5 ├── phparkitect.php ├── phpunit.xml ├── pint.json ├── public ├── .htaccess ├── favicon.ico ├── index.php └── robots.txt ├── rector.php └── sail / phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/nunomaduro/larastan/extension.neon 3 | 4 | parameters: 5 | paths: 6 | - App/ 7 | 8 | # Level 9 is the highest level 9 | level: 9 10 | 11 | # ignoreErrors: 12 | # - '#PHPDoc tag @var#' 13 | # 14 | excludePaths: 15 | - ./*/*/*.blade.php 16 | - ./App/Infrastructure/Laravel/Tests/*/*.php 17 | 18 | # checkMissingIterableValueType: false 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_URL=http://localhost 6 | APP_SERVICE=php 7 | APP_PORT=8000 8 | 9 | LOG_CHANNEL=stack 10 | LOG_DEPRECATIONS_CHANNEL=null 11 | LOG_LEVEL=debug 12 | 13 | DB_CONNECTION=mysql 14 | DB_HOST=127.0.0.1 15 | DB_PORT=3306 16 | DB_DATABASE=example 17 | DB_USERNAME=root 18 | DB_PASSWORD= 19 | 20 | BROADCAST_DRIVER=redis 21 | CACHE_DRIVER=redis 22 | FILESYSTEM_DISK=local 23 | QUEUE_CONNECTION=redis 24 | SESSION_DRIVER=redis 25 | SESSION_LIFETIME=120 26 | 27 | MEMCACHED_HOST=127.0.0.1 28 | 29 | REDIS_HOST=redis 30 | REDIS_PASSWORD=null 31 | REDIS_PORT=6379 32 | 33 | MAIL_MAILER=smtp 34 | MAIL_HOST=mailhog 35 | MAIL_PORT=1025 36 | MAIL_USERNAME=null 37 | MAIL_PASSWORD=null 38 | MAIL_ENCRYPTION=null 39 | MAIL_FROM_ADDRESS="hello@example.com" 40 | MAIL_FROM_NAME="${APP_NAME}" 41 | 42 | AWS_ACCESS_KEY_ID= 43 | AWS_SECRET_ACCESS_KEY= 44 | AWS_DEFAULT_REGION=us-east-1 45 | AWS_BUCKET= 46 | AWS_USE_PATH_STYLE_ENDPOINT=false 47 | 48 | PUSHER_APP_ID= 49 | PUSHER_APP_KEY= 50 | PUSHER_APP_SECRET= 51 | PUSHER_HOST= 52 | PUSHER_PORT=443 53 | PUSHER_SCHEME=https 54 | PUSHER_APP_CLUSTER=mt1 55 | 56 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 57 | VITE_PUSHER_HOST="${PUSHER_HOST}" 58 | VITE_PUSHER_PORT="${PUSHER_PORT}" 59 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 60 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 61 | 62 | APP_PACKAGES_CACHE="App/Infrastructure/Laravel/Bootstrap/cache/packages.php" 63 | APP_SERVICES_CACHE="App/Infrastructure/Laravel/Bootstrap/cache/services.php" 64 | 65 | SAIL_XDEBUG_MODE=develop,debug,coverage 66 | XDEBUG_SESSION=1 67 | 68 | MEMBER_ACTIVATION_URL=/api/v1/member-activations 69 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/build 3 | /public/hot 4 | /public/storage 5 | App/Infrastructure/Laravel/Storage/*.key 6 | 7 | /vendor 8 | .env 9 | .env.backup 10 | .phpunit.result.cache 11 | Homestead.json 12 | Homestead.yaml 13 | auth.json 14 | npm-debug.log 15 | yarn-error.log 16 | /.idea 17 | /.vscode 18 | .deptrac.cache 19 | 20 | App/Infrastructure/Laravel/Bootstrap/cache/* 21 | App/Infrastructure/Laravel/Storage/framework/cache/data/.gitignore 22 | App/Infrastructure/Laravel/Storage/framework/cache/data/* 23 | .phpstorm.meta.php 24 | -------------------------------------------------------------------------------- /App/Application/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnhubdev/laravel-api-starter-kit/bb9d0ffcba6555df9c95c6c47e9db1fc4a06537e/App/Application/.gitkeep -------------------------------------------------------------------------------- /App/Application/Events/EventDispatcher.php: -------------------------------------------------------------------------------- 1 | mailer->to(users: [$event->emailAddress->getValue()]) 32 | ->send( 33 | mailable: new MemberActivationEmail( 34 | firstName: $event->firstName, 35 | lastName: $event->lastName, 36 | configurationRepository: $this->configurationRepository 37 | ) 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /App/Application/Members/SignUpMember.php: -------------------------------------------------------------------------------- 1 | emailAddress); 37 | 38 | $emailAddressExists = $this->memberRepository->existsByEmailAddress(emailAddress: $emailAddress->getValue()); 39 | 40 | if ($emailAddressExists) { 41 | throw new EmailAddressIsAlreadyTaken(); 42 | } 43 | 44 | $member = Member::signUp( 45 | id: Id::createFromString(value: $this->memberRepository->generateIdentity()), 46 | firstName: FirstName::createFromString(value: $signUpMember->firstName), 47 | lastName: LastName::createFromString(value: $signUpMember->lastName), 48 | emailAddress: $emailAddress, 49 | status: StatusName::PENDING, 50 | createdAt: $this->clock->now(), 51 | updatedAt: $this->clock->now(), 52 | password: Password::createFromString(value: $this->hasher->make(value: $signUpMember->password)) 53 | ); 54 | 55 | $this->memberRepository->save(member: $member); 56 | 57 | $this->eventDispatcher->dispatchMultiple(events: $member->releaseEvents()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /App/Domain/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnhubdev/laravel-api-starter-kit/bb9d0ffcba6555df9c95c6c47e9db1fc4a06537e/App/Domain/.gitkeep -------------------------------------------------------------------------------- /App/Domain/Members/EmailAddress.php: -------------------------------------------------------------------------------- 1 | value = $value; 26 | } 27 | 28 | public function getValue(): string 29 | { 30 | return $this->value; 31 | } 32 | 33 | /** 34 | * @throws AssertionFailedException 35 | */ 36 | public static function createFromString(string $value): self 37 | { 38 | return new self(value: $value); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /App/Domain/Members/EmailAddressIsAlreadyTaken.php: -------------------------------------------------------------------------------- 1 | value = $value; 25 | } 26 | 27 | public function getValue(): string 28 | { 29 | return $this->value; 30 | } 31 | 32 | /** 33 | * @throws AssertionFailedException 34 | */ 35 | public static function createFromString(string $value): self 36 | { 37 | return new self(value: $value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /App/Domain/Members/Id.php: -------------------------------------------------------------------------------- 1 | value = $value; 25 | } 26 | 27 | public function getValue(): string 28 | { 29 | return $this->value; 30 | } 31 | 32 | /** 33 | * @return $this 34 | * 35 | * @throws AssertionFailedException 36 | */ 37 | public static function createFromString(string $value): self 38 | { 39 | return new self(value: $value); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /App/Domain/Members/LastName.php: -------------------------------------------------------------------------------- 1 | value = $value; 25 | } 26 | 27 | public function getValue(): string 28 | { 29 | return $this->value; 30 | } 31 | 32 | /** 33 | * @throws AssertionFailedException 34 | */ 35 | public static function createFromString(string $value): self 36 | { 37 | return new self(value: $value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /App/Domain/Members/Member.php: -------------------------------------------------------------------------------- 1 | raiseEvent( 49 | new MemberSignedUp( 50 | id: $id, 51 | firstName: $firstName, 52 | lastName: $lastName, 53 | emailAddress: $emailAddress, 54 | status: $status 55 | ) 56 | ); 57 | 58 | return $member; 59 | } 60 | 61 | public function markAsVerified(DateTimeImmutable $date): void 62 | { 63 | $this->emailVerifiedAt = $date; 64 | 65 | $this->raiseEvent(new MemberWasVerified(date: $date)); 66 | } 67 | 68 | /** 69 | * @return array 70 | */ 71 | public function mapForPersistence(): array 72 | { 73 | return [ 74 | 'id' => $this->id->getValue(), 75 | 'first_name' => $this->firstName->getValue(), 76 | 'last_name' => $this->lastName->getValue(), 77 | 'email' => $this->emailAddress->getValue(), 78 | 'status' => $this->status->value, 79 | 'password' => $this->password->getValue(), 80 | 'created_at' => $this->createdAt->format(format: 'Y-m-d H:i:s'), 81 | 'updated_at' => $this->updatedAt->format(format: 'Y-m-d H:i:s'), 82 | 'email_verified_at' => $this->emailVerifiedAt?->format(format: 'Y-m-d H:i:s'), 83 | ]; 84 | } 85 | 86 | public function raiseEvent(object $event): void 87 | { 88 | $this->events[] = $event; 89 | } 90 | 91 | /** 92 | * @return array 93 | */ 94 | public function releaseEvents(): array 95 | { 96 | $events = $this->events; 97 | 98 | $this->events = []; 99 | 100 | return $events; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /App/Domain/Members/MemberReadModel.php: -------------------------------------------------------------------------------- 1 | created_at; 35 | /** @var CarbonImmutable $updatedAt */ 36 | $updatedAt = $member->updated_at; 37 | /** @var CarbonImmutable|null $emailVerifiedAt */ 38 | $emailVerifiedAt = $member->email_verified_at; 39 | 40 | return new self( 41 | id: Id::createFromString(value: $member->id), 42 | firstName: FirstName::createFromString(value: $member->first_name), 43 | lastName: LastName::createFromString(value: $member->last_name), 44 | emailAddress: EmailAddress::createFromString(value: $member->email), 45 | status: StatusName::PENDING, 46 | createdAt: $createdAt, 47 | updatedAt: $updatedAt, 48 | emailVerifiedAt: $emailVerifiedAt ?? null 49 | ); 50 | } 51 | 52 | /** 53 | * @throws AssertionFailedException 54 | * @throws Exception 55 | */ 56 | public static function createFromArray(array $member): self 57 | { 58 | return new self( 59 | id: Id::createFromString(value: $member['id']), 60 | firstName: FirstName::createFromString(value: $member['firstName']), 61 | lastName: LastName::createFromString(value: $member['lastName']), 62 | emailAddress: EmailAddress::createFromString(value: $member['email']), 63 | status: StatusName::PENDING, 64 | createdAt: new DateTimeImmutable(datetime: $member['createdAt']), 65 | updatedAt: new DateTimeImmutable(datetime: $member['updatedAt']), 66 | emailVerifiedAt: isset($member['emailVerifiedAt']) ? new DateTimeImmutable($member['emailVerifiedAt']) : null 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /App/Domain/Members/MemberRepository.php: -------------------------------------------------------------------------------- 1 | value = $value; 25 | } 26 | 27 | public function getValue(): string 28 | { 29 | return $this->value; 30 | } 31 | 32 | /** 33 | * @throws AssertionFailedException 34 | */ 35 | public static function createFromString(string $value): self 36 | { 37 | return new self(value: $value); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /App/Domain/Members/StatusName.php: -------------------------------------------------------------------------------- 1 | self::TEXT_PENDING, 26 | self::ACTIVE => self::TEXT_ACTIVE, 27 | self::DEACTIVATED => self::TEXT_DEACTIVATED, 28 | self::DELETED => self::TEXT_DELETED, 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /App/Infrastructure/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnhubdev/laravel-api-starter-kit/bb9d0ffcba6555df9c95c6c47e9db1fc4a06537e/App/Infrastructure/.gitkeep -------------------------------------------------------------------------------- /App/Infrastructure/Authentication/User.php: -------------------------------------------------------------------------------- 1 | 66 | */ 67 | protected $hidden = [ 68 | 'password', 69 | 'remember_token', 70 | ]; 71 | 72 | /** 73 | * The attributes that should be cast. 74 | * 75 | * @var array 76 | */ 77 | protected $casts = [ 78 | 'id' => SnowflakeCast::class, 79 | 'first_name' => 'string', 80 | 'last_name' => 'string', 81 | 'email' => 'string', 82 | 'status' => StatusName::class, 83 | 'password' => 'string', 84 | 'remember_token' => 'string', 85 | 'created_at' => 'immutable_datetime', 86 | 'updated_at' => 'immutable_datetime', 87 | 'email_verified_at' => 'immutable_datetime', 88 | ]; 89 | } 90 | -------------------------------------------------------------------------------- /App/Infrastructure/Authentication/UserFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class UserFactory extends Factory 16 | { 17 | /** 18 | * Define the model's default state. 19 | * 20 | * @return array 21 | */ 22 | public function definition(): array 23 | { 24 | return [ 25 | 'first_name' => $this->faker->firstName(), 26 | 'last_name' => $this->faker->lastName(), 27 | 'email' => $this->faker->unique()->freeEmail(), 28 | 'password' => $this->faker->password(minLength: 8), 29 | 'status' => StatusName::PENDING->value, 30 | 'created_at' => Carbon::now(), 31 | 'updated_at' => Carbon::now(), 32 | 'email_verified_at' => null, 33 | 'remember_token' => Str::random(length: 10), 34 | ]; 35 | } 36 | 37 | /** 38 | * Indicate that the model's email address should be unverified. 39 | */ 40 | public function unverified(): self 41 | { 42 | return $this->state(fn (array $attributes) => [ 43 | 'email_verified_at' => null, 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /App/Infrastructure/Home/HomeController.php: -------------------------------------------------------------------------------- 1 | 'Welcome to the Laravel API Starter Kit Homepage', 18 | 'code' => Response::HTTP_OK, 19 | ]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 38 | abstract: HttpKernelContract::class, 39 | concrete: HttpKernel::class 40 | ); 41 | 42 | $app->singleton( 43 | abstract: ConsoleKernelContract::class, 44 | concrete: ConsoleKernel::class 45 | ); 46 | 47 | $app->singleton( 48 | abstract: ExceptionHandler::class, 49 | concrete: Handler::class 50 | ); 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Return The Application 55 | |-------------------------------------------------------------------------- 56 | | 57 | | This script returns the application instance. The instance is given to 58 | | the calling script so we can separate the building of the instances 59 | | from the actual running of the application and sending responses. 60 | | 61 | */ 62 | 63 | return $app; 64 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/auth.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'guard' => 'web', 20 | 'passwords' => 'users', 21 | ], 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Authentication Guards 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Next, you may define every authentication guard for your application. 29 | | Of course, a great default configuration has been defined for you 30 | | here which uses session storage and the Eloquent user provider. 31 | | 32 | | All authentication drivers have a user provider. This defines how the 33 | | users are actually retrieved out of your database or other storage 34 | | mechanisms used by this application to persist your user's data. 35 | | 36 | | Supported: "session" 37 | | 38 | */ 39 | 40 | 'guards' => [ 41 | 'web' => [ 42 | 'driver' => 'session', 43 | 'provider' => 'users', 44 | ], 45 | ], 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | User Providers 50 | |-------------------------------------------------------------------------- 51 | | 52 | | All authentication drivers have a user provider. This defines how the 53 | | users are actually retrieved out of your database or other storage 54 | | mechanisms used by this application to persist your user's data. 55 | | 56 | | If you have multiple user tables or models you may configure multiple 57 | | sources which represent each model / table. These sources may then 58 | | be assigned to any extra authentication guards you have defined. 59 | | 60 | | Supported: "database", "eloquent" 61 | | 62 | */ 63 | 64 | 'providers' => [ 65 | 'users' => [ 66 | 'driver' => 'eloquent', 67 | 'model' => User::class, 68 | ], 69 | 70 | // 'users' => [ 71 | // 'driver' => 'database', 72 | // 'table' => 'users', 73 | // ], 74 | ], 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Resetting Passwords 79 | |-------------------------------------------------------------------------- 80 | | 81 | | You may specify multiple password reset configurations if you have more 82 | | than one user table or model in the application and you want to have 83 | | separate password reset settings based on the specific user types. 84 | | 85 | | The expire time is the number of minutes that each reset token will be 86 | | considered valid. This security feature keeps tokens short-lived so 87 | | they have less time to be guessed. You may change this as needed. 88 | | 89 | */ 90 | 91 | 'passwords' => [ 92 | 'users' => [ 93 | 'provider' => 'users', 94 | 'table' => 'password_resets', 95 | 'expire' => 60, 96 | 'throttle' => 60, 97 | ], 98 | ], 99 | 100 | /* 101 | |-------------------------------------------------------------------------- 102 | | Password Confirmation Timeout 103 | |-------------------------------------------------------------------------- 104 | | 105 | | Here you may define the amount of seconds before a password confirmation 106 | | times out and the user is prompted to re-enter their password via the 107 | | confirmation screen. By default, the timeout lasts for three hours. 108 | | 109 | */ 110 | 111 | 'password_timeout' => 10800, 112 | 113 | ]; 114 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env(key: 'BROADCAST_DRIVER', default: 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env(key: 'PUSHER_APP_KEY'), 36 | 'secret' => env(key: 'PUSHER_APP_SECRET'), 37 | 'app_id' => env(key: 'PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'host' => env(key: 'PUSHER_HOST', default: 'api-'.env(key: 'PUSHER_APP_CLUSTER', default: 'mt1').'.pusher.com') ?: 'api-'.env(key: 'PUSHER_APP_CLUSTER', default: 'mt1').'.pusher.com', 40 | 'port' => env(key: 'PUSHER_PORT', default: 443), 41 | 'scheme' => env(key: 'PUSHER_SCHEME', default: 'https'), 42 | 'encrypted' => true, 43 | 'useTLS' => env(key: 'PUSHER_SCHEME', default: 'https') === 'https', 44 | ], 45 | 'client_options' => [ 46 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 47 | ], 48 | ], 49 | 50 | 'ably' => [ 51 | 'driver' => 'ably', 52 | 'key' => env(key: 'ABLY_KEY'), 53 | ], 54 | 55 | 'redis' => [ 56 | 'driver' => 'redis', 57 | 'connection' => 'default', 58 | ], 59 | 60 | 'log' => [ 61 | 'driver' => 'log', 62 | ], 63 | 64 | 'null' => [ 65 | 'driver' => 'null', 66 | ], 67 | 68 | ], 69 | 70 | ]; 71 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/cache.php: -------------------------------------------------------------------------------- 1 | env(key: 'CACHE_DRIVER', default: 'file'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Cache Stores 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the cache "stores" for your application as 26 | | well as their drivers. You may even define multiple stores for the 27 | | same cache driver to group types of items stored in your caches. 28 | | 29 | | Supported drivers: "apc", "array", "database", "file", 30 | | "memcached", "redis", "dynamodb", "octane", "null" 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'apc' => [ 37 | 'driver' => 'apc', 38 | ], 39 | 40 | 'array' => [ 41 | 'driver' => 'array', 42 | 'serialize' => false, 43 | ], 44 | 45 | 'database' => [ 46 | 'driver' => 'database', 47 | 'table' => 'cache', 48 | 'connection' => null, 49 | 'lock_connection' => null, 50 | ], 51 | 52 | 'file' => [ 53 | 'driver' => 'file', 54 | 'path' => storage_path('framework/cache/data'), 55 | ], 56 | 57 | 'memcached' => [ 58 | 'driver' => 'memcached', 59 | 'persistent_id' => env(key: 'MEMCACHED_PERSISTENT_ID'), 60 | 'sasl' => [ 61 | env(key: 'MEMCACHED_USERNAME'), 62 | env(key: 'MEMCACHED_PASSWORD'), 63 | ], 64 | 'options' => [ 65 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 66 | ], 67 | 'servers' => [ 68 | [ 69 | 'host' => env(key: 'MEMCACHED_HOST', default: '127.0.0.1'), 70 | 'port' => env(key: 'MEMCACHED_PORT', default: 11211), 71 | 'weight' => 100, 72 | ], 73 | ], 74 | ], 75 | 76 | 'redis' => [ 77 | 'driver' => 'redis', 78 | 'connection' => 'cache', 79 | 'lock_connection' => 'default', 80 | ], 81 | 82 | 'dynamodb' => [ 83 | 'driver' => 'dynamodb', 84 | 'key' => env(key: 'AWS_ACCESS_KEY_ID'), 85 | 'secret' => env(key: 'AWS_SECRET_ACCESS_KEY'), 86 | 'region' => env(key: 'AWS_DEFAULT_REGION', default: 'us-east-1'), 87 | 'table' => env(key: 'DYNAMODB_CACHE_TABLE', default: 'cache'), 88 | 'endpoint' => env(key: 'DYNAMODB_ENDPOINT'), 89 | ], 90 | 91 | 'octane' => [ 92 | 'driver' => 'octane', 93 | ], 94 | 95 | ], 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Cache Key Prefix 100 | |-------------------------------------------------------------------------- 101 | | 102 | | When utilizing the APC, database, memcached, Redis, or DynamoDB cache 103 | | stores there might be other applications using the same cache. For 104 | | that reason, you may prefix every cache key to avoid collisions. 105 | | 106 | */ 107 | 108 | 'prefix' => env(key: 'CACHE_PREFIX', default: Str::slug(title: env(key: 'APP_NAME', default: 'laravel'), separator: '_').'_cache_'), 109 | 110 | ]; 111 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/filesystems.php: -------------------------------------------------------------------------------- 1 | env(key: 'FILESYSTEM_DISK', default: 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure as many filesystem "disks" as you wish, and you 24 | | may even configure multiple disks of the same driver. Defaults have 25 | | been set up for each driver as an example of the required values. 26 | | 27 | | Supported Drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path(path: 'app'), 36 | 'throw' => false, 37 | ], 38 | 39 | 'public' => [ 40 | 'driver' => 'local', 41 | 'root' => storage_path(path: 'app/public'), 42 | 'url' => env(key: 'APP_URL').'/storage', 43 | 'visibility' => 'public', 44 | 'throw' => false, 45 | ], 46 | 47 | 's3' => [ 48 | 'driver' => 's3', 49 | 'key' => env(key: 'AWS_ACCESS_KEY_ID'), 50 | 'secret' => env(key: 'AWS_SECRET_ACCESS_KEY'), 51 | 'region' => env(key: 'AWS_DEFAULT_REGION'), 52 | 'bucket' => env(key: 'AWS_BUCKET'), 53 | 'url' => env(key: 'AWS_URL'), 54 | 'endpoint' => env(key: 'AWS_ENDPOINT'), 55 | 'use_path_style_endpoint' => env(key: 'AWS_USE_PATH_STYLE_ENDPOINT', default: false), 56 | 'throw' => false, 57 | ], 58 | 59 | ], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Symbolic Links 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Here you may configure the symbolic links that will be created when the 67 | | `storage:link` Artisan command is executed. The array keys should be 68 | | the locations of the links and the values should be their targets. 69 | | 70 | */ 71 | 72 | 'links' => [ 73 | public_path(path: 'storage') => storage_path(path: 'app/public'), 74 | ], 75 | 76 | ]; 77 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/flare.php: -------------------------------------------------------------------------------- 1 | env(key: 'FLARE_KEY'), 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Middleware 33 | |-------------------------------------------------------------------------- 34 | | 35 | | These middleware will modify the contents of the report sent to Flare. 36 | | 37 | */ 38 | 39 | 'flare_middleware' => [ 40 | RemoveRequestIp::class, 41 | AddGitInformation::class, 42 | AddNotifierName::class, 43 | AddEnvironmentInformation::class, 44 | AddExceptionInformation::class, 45 | AddDumps::class, 46 | AddLogs::class => [ 47 | 'maximum_number_of_collected_logs' => 200, 48 | ], 49 | AddQueries::class => [ 50 | 'maximum_number_of_collected_queries' => 200, 51 | 'report_query_bindings' => true, 52 | ], 53 | AddJobs::class => [ 54 | 'max_chained_job_reporting_depth' => 5, 55 | ], 56 | CensorRequestBodyFields::class => [ 57 | 'censor_fields' => [ 58 | 'password', 59 | 'password_confirmation', 60 | ], 61 | ], 62 | CensorRequestHeaders::class => [ 63 | 'headers' => [ 64 | 'API-KEY', 65 | ], 66 | ], 67 | ], 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Reporting log statements 72 | |-------------------------------------------------------------------------- 73 | | 74 | | If this setting is `false` log statements won't be sent as events to Flare, 75 | | no matter which error level you specified in the Flare log channel. 76 | | 77 | */ 78 | 79 | 'send_logs_as_events' => true, 80 | ]; 81 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env(key: 'BCRYPT_ROUNDS', default: 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 65536, 48 | 'threads' => 1, 49 | 'time' => 4, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/members.php: -------------------------------------------------------------------------------- 1 | env(key: 'MEMBER_ACTIVATION_URL'), 5 | ]; 6 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/queue.php: -------------------------------------------------------------------------------- 1 | env(key: 'QUEUE_CONNECTION', default: 'sync'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Queue Connections 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure the connection information for each server that 24 | | is used by your application. A default configuration has been added 25 | | for each back-end shipped with Laravel. You are free to add more. 26 | | 27 | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'sync' => [ 34 | 'driver' => 'sync', 35 | ], 36 | 37 | 'database' => [ 38 | 'driver' => 'database', 39 | 'table' => 'jobs', 40 | 'queue' => 'default', 41 | 'retry_after' => 90, 42 | 'after_commit' => false, 43 | ], 44 | 45 | 'beanstalkd' => [ 46 | 'driver' => 'beanstalkd', 47 | 'host' => 'localhost', 48 | 'queue' => 'default', 49 | 'retry_after' => 90, 50 | 'block_for' => 0, 51 | 'after_commit' => false, 52 | ], 53 | 54 | 'sqs' => [ 55 | 'driver' => 'sqs', 56 | 'key' => env(key: 'AWS_ACCESS_KEY_ID'), 57 | 'secret' => env(key: 'AWS_SECRET_ACCESS_KEY'), 58 | 'prefix' => env(key: 'SQS_PREFIX', default: 'https://sqs.us-east-1.amazonaws.com/your-account-id'), 59 | 'queue' => env(key: 'SQS_QUEUE', default: 'default'), 60 | 'suffix' => env(key: 'SQS_SUFFIX'), 61 | 'region' => env(key: 'AWS_DEFAULT_REGION', default: 'us-east-1'), 62 | 'after_commit' => false, 63 | ], 64 | 65 | 'redis' => [ 66 | 'driver' => 'redis', 67 | 'connection' => 'default', 68 | 'queue' => env(key: 'REDIS_QUEUE', default: 'default'), 69 | 'retry_after' => 90, 70 | 'block_for' => null, 71 | 'after_commit' => false, 72 | ], 73 | 74 | ], 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | Job Batching 79 | |-------------------------------------------------------------------------- 80 | | 81 | | The following options configure the database and table that store job 82 | | batching information. These options can be updated to any database 83 | | connection and table which has been defined by your application. 84 | | 85 | */ 86 | 'batching' => [ 87 | 'database' => env(key: 'DB_CONNECTION', default: 'mysql'), 88 | 'table' => 'job_batches', 89 | ], 90 | 91 | 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Failed Queue Jobs 95 | |-------------------------------------------------------------------------- 96 | | 97 | | These options configure the behavior of failed queue job logging so you 98 | | can control which database and table are used to store the jobs that 99 | | have failed. You may change them to any database / table you wish. 100 | | 101 | */ 102 | 103 | 'failed' => [ 104 | 'driver' => env(key: 'QUEUE_FAILED_DRIVER', default: 'database-uuids'), 105 | 'database' => env(key: 'DB_CONNECTION', default: 'mysql'), 106 | 'table' => 'failed_jobs', 107 | ], 108 | 109 | ]; 110 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/route-attributes.php: -------------------------------------------------------------------------------- 1 | true, 8 | 9 | /* 10 | * Controllers in these directories that have routing attributes 11 | * will automatically be registered. 12 | * 13 | * Optionally, you can specify group configuration by using key/values 14 | */ 15 | 'directories' => [ 16 | base_path(path: 'App/Infrastructure/Members') => [ 17 | 'middleware' => 'api', 18 | 'prefix' => 'api/v1', 19 | 'namespace' => '\App\Infrastructure\Members', 20 | ], 21 | base_path(path: 'App/Infrastructure/Home') => [ 22 | 'middleware' => 'api', 23 | 'prefix' => 'api/v1', 24 | 'namespace' => '\App\Infrastructure\Home', 25 | ], 26 | ], 27 | 28 | /** 29 | * This middleware will be applied to all routes. 30 | */ 31 | 'middleware' => [], 32 | ]; 33 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', (string) env('SANCTUM_STATEFUL_DOMAINS', sprintf( 21 | '%s%s', 22 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 23 | Sanctum::currentApplicationUrlWithPort() 24 | ))), 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Sanctum Guards 29 | |-------------------------------------------------------------------------- 30 | | 31 | | This array contains the authentication guards that will be checked when 32 | | Sanctum is trying to authenticate a request. If none of these guards 33 | | are able to authenticate the request, Sanctum will use the bearer 34 | | token that's present on an incoming request for authentication. 35 | | 36 | */ 37 | 38 | 'guard' => ['web'], 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Expiration Minutes 43 | |-------------------------------------------------------------------------- 44 | | 45 | | This value controls the number of minutes until an issued token will be 46 | | considered expired. If this value is null, personal access tokens do 47 | | not expire. This won't tweak the lifetime of first-party sessions. 48 | | 49 | */ 50 | 51 | 'expiration' => null, 52 | 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | Sanctum Middleware 56 | |-------------------------------------------------------------------------- 57 | | 58 | | When authenticating your first-party SPA with Sanctum you may need to 59 | | customize some of the middleware Sanctum uses while processing the 60 | | request. You may change the middleware listed below as required. 61 | | 62 | */ 63 | 64 | 'middleware' => [ 65 | 'verify_csrf_token' => VerifyCsrfToken::class, 66 | 'encrypt_cookies' => EncryptCookies::class, 67 | ], 68 | 69 | ]; 70 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env(key: 'MAILGUN_DOMAIN'), 19 | 'secret' => env(key: 'MAILGUN_SECRET'), 20 | 'endpoint' => env(key: 'MAILGUN_ENDPOINT', default: 'api.mailgun.net'), 21 | 'scheme' => 'https', 22 | ], 23 | 24 | 'postmark' => [ 25 | 'token' => env(key: 'POSTMARK_TOKEN'), 26 | ], 27 | 28 | 'ses' => [ 29 | 'key' => env(key: 'AWS_ACCESS_KEY_ID'), 30 | 'secret' => env(key: 'AWS_SECRET_ACCESS_KEY'), 31 | 'region' => env(key: 'AWS_DEFAULT_REGION', default: 'us-east-1'), 32 | ], 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/snowflake.php: -------------------------------------------------------------------------------- 1 | true, 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Data Center 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This value represents the data center reference that should be used by 28 | | Snowflake when generating unique identifiers. The value must be 1 - 31. 29 | | 30 | */ 31 | 32 | 'data_center' => 1, 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Worker Node 37 | |-------------------------------------------------------------------------- 38 | | 39 | | This value represents the worker node reference that should be used by 40 | | Snowflake when generating unique identifiers. The value must be 1 - 31. 41 | | 42 | */ 43 | 44 | 'worker_node' => 1, 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Start Timestamp 49 | |-------------------------------------------------------------------------- 50 | | 51 | | This value represents the starting date for generating new timestamps. 52 | | Snowflakes can be created for 69 years past this date. In most cases, 53 | | you should set this value to the current date when building a new app. 54 | | 55 | */ 56 | 57 | 'start_timestamp' => '2022-11-01', 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Sequence Resolver 62 | |-------------------------------------------------------------------------- 63 | | 64 | | This value represents the sequencing strategy that should be used to 65 | | ensure that multiple Snowflakes generated within the same millisecond 66 | | are unique. The default is a good choice, as it has no dependencies. 67 | | 68 | */ 69 | 70 | 'sequence_resolver' => RandomSequenceResolver::class, 71 | ]; 72 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/tinker.php: -------------------------------------------------------------------------------- 1 | [ 17 | // App\Console\Commands\ExampleCommand::class, 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Auto Aliased Classes 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Tinker will not automatically alias classes in your vendor namespaces 26 | | but you may explicitly allow a subset of classes to get aliased by 27 | | adding the names of each of those classes to the following list. 28 | | 29 | */ 30 | 31 | 'alias' => [ 32 | // 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Classes That Should Not Be Aliased 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Typically, Tinker automatically aliases classes as you require them in 41 | | Tinker. However, you may wish to never alias certain classes, which 42 | | you may accomplish by listing the classes in the following array. 43 | | 44 | */ 45 | 46 | 'dont_alias' => [ 47 | 'App\Nova', 48 | ], 49 | 50 | ]; 51 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'App/Infrastructure', 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | key: 'VIEW_COMPILED_PATH', 33 | default: realpath(path: storage_path(path: 'framework/views')) 34 | ), 35 | ]; 36 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 18 | } 19 | 20 | /** 21 | * Register the commands for the application. 22 | */ 23 | protected function commands(): void 24 | { 25 | $this->load([]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Database/Migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | snowflake()->primary(); 19 | $table->string(column: 'first_name', length: 100); 20 | $table->string(column: 'last_name', length: 100); 21 | $table->string(column: 'email', length: 200)->unique(); 22 | $table->timestamp(column: 'email_verified_at')->nullable(); 23 | $table->string(column: 'password'); 24 | $table->unsignedTinyInteger(column: 'status')->default(value: StatusName::PENDING->value); 25 | $table->string(column: 'remember_token', length: 100)->nullable(); 26 | $table->timestamp(column: 'created_at')->nullable(); 27 | $table->timestamp(column: 'updated_at')->nullable(); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | Schema::dropIfExists(table: 'users'); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Database/Migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string(column: 'email', length: 200)->index(); 18 | $table->string(column: 'token'); 19 | $table->timestamp(column: 'created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | */ 26 | public function down(): void 27 | { 28 | Schema::dropIfExists(table: 'password_reset_tokens'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Database/Migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | snowflake()->primary(); 20 | $table->string(column: 'uuid')->unique(); 21 | $table->text(column: 'connection'); 22 | $table->text(column: 'queue'); 23 | $table->longText(column: 'payload'); 24 | $table->longText(column: 'exception'); 25 | $table->timestamp(column: 'failed_at')->useCurrent(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::dropIfExists(table: 'failed_jobs'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Database/Migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | snowflake()->primary(); 18 | $table->morphs(name: 'tokenable'); 19 | $table->string(column: 'name'); 20 | $table->string(column: 'token', length: 64)->unique(); 21 | $table->text(column: 'abilities')->nullable(); 22 | $table->timestamp(column: 'last_used_at')->nullable(); 23 | $table->timestamp(column: 'expires_at')->nullable(); 24 | $table->timestamp(column: 'created_at')->nullable(); 25 | $table->timestamp(column: 'updated_at')->nullable(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::dropIfExists(table: 'personal_access_tokens'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Database/Seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 18 | 19 | // \App\Models\User::factory()->create([ 20 | // 'name' => 'Test User', 21 | // 'email' => 'test@example.com', 22 | // ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Events/Dispatcher.php: -------------------------------------------------------------------------------- 1 | flush(event: $event::class); 16 | } 17 | } 18 | 19 | public function dispatchMultiple(array $events, bool $halt = false): void 20 | { 21 | foreach ($events as $event) { 22 | $this->dispatch(event: $event::class, payload: get_class_vars($event::class), halt: $halt); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Events/FakeEventDispatcher.php: -------------------------------------------------------------------------------- 1 | flush(event: $event::class); 16 | } 17 | } 18 | 19 | public function dispatchMultiple(array $events, bool $halt = false): void 20 | { 21 | foreach ($events as $event) { 22 | $this->dispatch(event: $event::class, payload: get_class_vars($event::class), halt: $halt); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Events/NullDispatcher.php: -------------------------------------------------------------------------------- 1 | flush(event: $event::class); 16 | } 17 | } 18 | 19 | public function dispatchMultiple(array $events, bool $halt = false): void 20 | { 21 | foreach ($events as $event) { 22 | $this->dispatch(event: $event::class, payload: get_class_vars($event::class), halt: $halt); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | , LogLevel::*> 17 | */ 18 | protected $levels = [ 19 | // 20 | ]; 21 | 22 | /** 23 | * A list of the exception types that are not reported. 24 | * 25 | * @var array> 26 | */ 27 | protected $dontReport = [ 28 | // 29 | ]; 30 | 31 | /** 32 | * A list of the inputs that are never flashed to the session on validation exceptions. 33 | * 34 | * @var array 35 | */ 36 | protected $dontFlash = [ 37 | 'current_password', 38 | 'password', 39 | 'password_confirmation', 40 | ]; 41 | 42 | /** 43 | * Register the exception handling callbacks for the application. 44 | */ 45 | public function register(): void 46 | { 47 | $this->reportable(reportUsing: function (Throwable $e) { 48 | // 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Foundation/Application.php: -------------------------------------------------------------------------------- 1 | setBasePath($basePath); 18 | } 19 | 20 | $this->registerBaseBindings(); 21 | $this->registerBaseServiceProviders(); 22 | $this->registerCoreContainerAliases(); 23 | } 24 | 25 | /** 26 | * Bind all the application paths in the container. 27 | */ 28 | protected function bindPathsInContainer(): void 29 | { 30 | $this->instance(abstract: 'path', instance: $this->path('App/Infrastructure/Laravel/')); 31 | $this->instance(abstract: 'path.base', instance: $this->basePath()); 32 | $this->instance(abstract: 'path.config', instance: $this->configPath(path: 'App/Infrastructure/Laravel/Config')); 33 | $this->instance(abstract: 'path.public', instance: $this->publicPath()); 34 | $this->instance(abstract: 'path.storage', instance: $this->storagePath(path: 'App/Infrastructure/Laravel/Storage')); 35 | $this->instance(abstract: 'path.database', instance: $this->databasePath(path: 'App/Infrastructure/Laravel/Database')); 36 | $this->instance(abstract: 'path.resources', instance: $this->resourcePath(path: 'App/Infrastructure/Laravel/Resources')); 37 | $this->instance(abstract: 'path.bootstrap', instance: $this->bootstrapPath(path: 'App/Infrastructure/Laravel/Bootstrap')); 38 | $this->useDatabasePath(path: dirname(path: __DIR__).'/Database'); 39 | $this->useAppPath(path: dirname(path: __DIR__)); 40 | $this->useStoragePath(path: dirname(path: __DIR__).'/Storage'); 41 | $this->useLangPath(path: value(value: function () { 42 | if (is_dir(filename: $directory = $this->resourcePath(path: 'Language'))) { 43 | return $directory; 44 | } 45 | 46 | return $this->basePath(path: 'App/Infrastructure/Laravel/Language'); 47 | })); 48 | } 49 | 50 | /** 51 | * Get the path to the application configuration files. 52 | * 53 | * @param string $path 54 | */ 55 | public function configPath($path = ''): string 56 | { 57 | return dirname(path: __DIR__).'/Config'.($path != '' ? DIRECTORY_SEPARATOR.$path : ''); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | protected $middleware = [ 37 | //TrustHosts::class, 38 | TrustProxies::class, 39 | HandleCors::class, 40 | PreventRequestsDuringMaintenance::class, 41 | ValidatePostSize::class, 42 | TrimStrings::class, 43 | ConvertEmptyStringsToNull::class, 44 | ]; 45 | 46 | /** 47 | * The application's route middleware groups. 48 | * 49 | * @var array> 50 | */ 51 | protected $middlewareGroups = [ 52 | 'web' => [ 53 | EncryptCookies::class, 54 | AddQueuedCookiesToResponse::class, 55 | StartSession::class, 56 | ShareErrorsFromSession::class, 57 | VerifyCsrfToken::class, 58 | ], 59 | 60 | 'api' => [ 61 | ThrottleRequests::class.':api', 62 | ], 63 | ]; 64 | 65 | /** 66 | * The application's middleware aliases. 67 | * 68 | * Aliases may be used instead of class names to conveniently assign middleware to routes and groups. 69 | * 70 | * @var array 71 | */ 72 | protected $middlewareAliases = [ 73 | 'auth' => Authenticate::class, 74 | 'cache.headers' => SetCacheHeaders::class, 75 | 'can' => Authorize::class, 76 | 'password.confirm' => RequirePassword::class, 77 | 'signed' => ValidateSignature::class, 78 | 'throttle' => ThrottleRequests::class, 79 | 'verified' => EnsureEmailIsVerified::class, 80 | ]; 81 | } 82 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $except = [ 17 | // 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $except = [ 17 | // 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $except = [ 17 | 'current_password', 18 | 'password', 19 | 'password_confirmation', 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function hosts(): array 17 | { 18 | return [ 19 | $this->allSubdomainsOfApplicationUrl(), 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 16 | */ 17 | protected $proxies; 18 | 19 | /** 20 | * The headers that should be used to detect proxies. 21 | * 22 | * @var int 23 | */ 24 | protected $headers = 25 | Request::HEADER_X_FORWARDED_FOR | 26 | Request::HEADER_X_FORWARDED_HOST | 27 | Request::HEADER_X_FORWARDED_PORT | 28 | Request::HEADER_X_FORWARDED_PROTO | 29 | Request::HEADER_X_FORWARDED_AWS_ELB; 30 | } 31 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Http/Middleware/ValidateSignature.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected array $except = [ 17 | // 'fbclid', 18 | // 'utm_campaign', 19 | // 'utm_content', 20 | // 'utm_medium', 21 | // 'utm_source', 22 | // 'utm_term', 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Http/Middleware/VerifyCsrfToken.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $except = [ 17 | // 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Language/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Language/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Language/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Models/BaseModel.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $policies = []; 17 | 18 | /** 19 | * Register any authentication / authorization services. 20 | */ 21 | public function boot(): void 22 | { 23 | // 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Providers/BroadcastServiceProvider.php: -------------------------------------------------------------------------------- 1 | SignUpMemberCommandHandler::class, 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Providers/ClockServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(abstract: ClockInterface::class, concrete: NativeClock::class); 19 | } 20 | 21 | /** 22 | * Bootstrap services. 23 | */ 24 | public function boot(): void 25 | { 26 | // 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 19 | */ 20 | protected $listen = [ 21 | MemberSignedUp::class => [ 22 | SendMemberActivationEmail::class, 23 | ], 24 | ]; 25 | 26 | /** 27 | * Register any events for your application. 28 | */ 29 | public function boot(): void 30 | { 31 | // 32 | } 33 | 34 | public function register(): void 35 | { 36 | $this->app->singleton(abstract: EventDispatcher::class, concrete: Dispatcher::class); 37 | } 38 | 39 | /** 40 | * Determine if events and listeners should be automatically discovered. 41 | */ 42 | public function shouldDiscoverEvents(): bool 43 | { 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Providers/MemberServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton(abstract: MemberRepository::class, concrete: EloquentMemberRepository::class); 19 | } 20 | 21 | /** 22 | * Bootstrap services. 23 | */ 24 | public function boot(): void 25 | { 26 | // 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Providers/ModelFactoryServiceProvider.php: -------------------------------------------------------------------------------- 1 | $modelName.'Factory'); 16 | } 17 | 18 | /** 19 | * Bootstrap services. 20 | */ 21 | public function boot(): void 22 | { 23 | // 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Providers/ModelServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->isProduction()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Providers/RelationServiceProvider.php: -------------------------------------------------------------------------------- 1 | User::class, 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Providers/RouteAttributesServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(path: config_path(path: 'route-attributes.php'), key: 'route-attributes'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 20 | } 21 | 22 | /** 23 | * Configure the rate limiters for the application. 24 | */ 25 | protected function configureRateLimiting(): void 26 | { 27 | RateLimiter::for(name: 'api', callback: fn(Request $request) => Limit::perMinute(maxAttempts: 60)->by(key: $request->user()?->id ?: $request->ip())); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/errors/401.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Unauthorized')) 4 | @section('code', '401') 5 | @section('message', __('Unauthorized')) 6 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/errors/403.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Forbidden')) 4 | @section('code', '403') 5 | @section('message', __($exception->getMessage() ?: 'Forbidden')) 6 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/errors/404.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Not Found')) 4 | @section('code', '404') 5 | @section('message', __('Not Found')) 6 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/errors/419.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Page Expired')) 4 | @section('code', '419') 5 | @section('message', __('Page Expired')) 6 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/errors/429.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Too Many Requests')) 4 | @section('code', '429') 5 | @section('message', __('Too Many Requests')) 6 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/errors/500.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Server Error')) 4 | @section('code', '500') 5 | @section('message', __('Server Error')) 6 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/errors/503.blade.php: -------------------------------------------------------------------------------- 1 | @extends('errors::minimal') 2 | 3 | @section('title', __('Service Unavailable')) 4 | @section('code', '503') 5 | @section('message', __('Service Unavailable')) 6 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/errors/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | @yield('title') 8 | 9 | 10 | 43 | 44 | 45 |
46 |
47 |
48 | @yield('message') 49 |
50 |
51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/html/button.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'url', 3 | 'color' => 'primary', 4 | ]) 5 | 6 | 7 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/html/footer.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/html/header.blade.php: -------------------------------------------------------------------------------- 1 | @props(['url']) 2 | 3 | 4 | 5 | @if (trim($slot) === 'Laravel') 6 | 7 | @else 8 | {{ $slot }} 9 | @endif 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/html/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 25 | 26 | 27 | 28 | 29 | 30 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/html/message.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Header --}} 3 | 4 | 5 | {{ config('app.name') }} 6 | 7 | 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | 15 | 16 | {{ $subcopy }} 17 | 18 | 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | 23 | 24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/html/panel.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/html/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/html/table.blade.php: -------------------------------------------------------------------------------- 1 |
2 | {{ Illuminate\Mail\Markdown::parse($slot) }} 3 |
4 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/text/button.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }}: {{ $url }} 2 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/text/footer.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/text/header.blade.php: -------------------------------------------------------------------------------- 1 | [{{ $slot }}]({{ $url }}) 2 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/text/layout.blade.php: -------------------------------------------------------------------------------- 1 | {!! strip_tags($header) !!} 2 | 3 | {!! strip_tags($slot) !!} 4 | @isset($subcopy) 5 | 6 | {!! strip_tags($subcopy) !!} 7 | @endisset 8 | 9 | {!! strip_tags($footer) !!} 10 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/text/message.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Header --}} 3 | 4 | 5 | {{ config('app.name') }} 6 | 7 | 8 | 9 | {{-- Body --}} 10 | {{ $slot }} 11 | 12 | {{-- Subcopy --}} 13 | @isset($subcopy) 14 | 15 | 16 | {{ $subcopy }} 17 | 18 | 19 | @endisset 20 | 21 | {{-- Footer --}} 22 | 23 | 24 | © {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/text/panel.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/text/subcopy.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/mail/text/table.blade.php: -------------------------------------------------------------------------------- 1 | {{ $slot }} 2 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Resources/Views/vendor/notifications/email.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{-- Greeting --}} 3 | @if (! empty($greeting)) 4 | # {{ $greeting }} 5 | @else 6 | @if ($level === 'error') 7 | # @lang('Whoops!') 8 | @else 9 | # @lang('Hello!') 10 | @endif 11 | @endif 12 | 13 | {{-- Intro Lines --}} 14 | @foreach ($introLines as $line) 15 | {{ $line }} 16 | 17 | @endforeach 18 | 19 | {{-- Action Button --}} 20 | @isset($actionText) 21 | $level, 24 | default => 'primary', 25 | }; 26 | ?> 27 | 28 | {{ $actionText }} 29 | 30 | @endisset 31 | 32 | {{-- Outro Lines --}} 33 | @foreach ($outroLines as $line) 34 | {{ $line }} 35 | 36 | @endforeach 37 | 38 | {{-- Salutation --}} 39 | @if (! empty($salutation)) 40 | {{ $salutation }} 41 | @else 42 | @lang('Regards'),
43 | {{ config('app.name') }} 44 | @endif 45 | 46 | {{-- Subcopy --}} 47 | @isset($actionText) 48 | 49 | @lang( 50 | "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n". 51 | 'into your web browser:', 52 | [ 53 | 'actionText' => $actionText, 54 | ] 55 | ) [{{ $displayableActionUrl }}]({{ $actionUrl }}) 56 | 57 | @endisset 58 |
59 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Storage/Logs/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | laravel-* 3 | laravel.log 4 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | public 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | compiled.php 2 | config.php 3 | down 4 | events.scanned.php 5 | maintenance.php 6 | routes.php 7 | routes.scanned.php 8 | schedule-* 9 | services.json 10 | !.gitignore 11 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | data 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Storage/framework/sessions/uHxTGZFnEW8wUYP2fcXhJLrzjWzmY8hFdX6EYoSF: -------------------------------------------------------------------------------- 1 | a:3:{s:6:"_token";s:40:"adpo6hmUEAXWI8Ut0PRXECPCS2KEaXSebWAiPYhB";s:9:"_previous";a:1:{s:3:"url";s:21:"http://localhost:8000";}s:6:"_flash";a:2:{s:3:"old";a:0:{}s:3:"new";a:0:{}}} -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | *.php 3 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Tests/Application/Members/SignUpMemberCommandHandlerTest.php: -------------------------------------------------------------------------------- 1 | memberRepository = new ArrayMemberRepository(); 26 | $this->eventDispatcher = new FakeEventDispatcher(dispatcher: new Dispatcher()); 27 | $this->clock = new MockClock(); 28 | $this->hasher = new BcryptHasher(); 29 | } 30 | 31 | /** 32 | * @test 33 | * 34 | * @throws EmailAddressIsAlreadyTaken 35 | * @throws AssertionFailedException 36 | * @throws MemberWasNotFound 37 | */ 38 | public function a_new_member_can_sign_up(): void 39 | { 40 | $faker = Factory::create(); 41 | 42 | $signUpMemberCommandHandler = new SignUpMemberCommandHandler( 43 | memberRepository: $this->memberRepository, 44 | eventDispatcher: $this->eventDispatcher, 45 | clock: $this->clock, 46 | hasher: $this->hasher 47 | ); 48 | 49 | $emailAddress = $faker->unique()->freeEmail(); 50 | 51 | $signUpMemberCommandHandler->handle( 52 | new SignUpMember( 53 | firstName: $faker->firstName(), 54 | lastName: $faker->lastName(), 55 | emailAddress: $emailAddress, 56 | password: $faker->password() 57 | ) 58 | ); 59 | 60 | $this->eventDispatcher->assertDispatched(event: MemberSignedUp::class); 61 | 62 | $this->assertNotNull(actual: $this->memberRepository->findByEmailAddress($emailAddress)); 63 | $this->assertTrue(condition: $this->memberRepository->existsByEmailAddress($emailAddress)); 64 | } 65 | 66 | /** 67 | * @test 68 | * 69 | * @throws EmailAddressIsAlreadyTaken 70 | * @throws AssertionFailedException 71 | */ 72 | public function a_new_member_cannot_sign_up_with_an_existing_email_address(): void 73 | { 74 | $this->expectException(exception: EmailAddressIsAlreadyTaken::class); 75 | 76 | $faker = Factory::create(); 77 | 78 | $signUpMemberCommandHandler = new SignUpMemberCommandHandler( 79 | memberRepository: $this->memberRepository, 80 | eventDispatcher: $this->eventDispatcher, 81 | clock: $this->clock, 82 | hasher: $this->hasher 83 | ); 84 | 85 | $emailAddress = 'myemailaddress@gmail.com'; 86 | 87 | $signUpMemberCommandHandler->handle( 88 | new SignUpMember( 89 | firstName: $faker->firstName(), 90 | lastName: $faker->lastName(), 91 | emailAddress: $emailAddress, 92 | password: $faker->password() 93 | ) 94 | ); 95 | 96 | $signUpMemberCommandHandler->handle( 97 | new SignUpMember( 98 | firstName: $faker->firstName(), 99 | lastName: $faker->lastName(), 100 | emailAddress: $emailAddress, 101 | password: $faker->password() 102 | ) 103 | ); 104 | 105 | $this->eventDispatcher->assertDispatchedTimes(event: MemberSignedUp::class, times: 1); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(abstract: Kernel::class)->bootstrap(); 20 | 21 | return $app; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Tests/Domain/Members/MemberTest.php: -------------------------------------------------------------------------------- 1 | memberRepository = new ArrayMemberRepository(); 27 | $this->clock = new MockClock(); 28 | $this->hasher = new BcryptHasher(); 29 | } 30 | 31 | /** 32 | * @test 33 | * 34 | * @throws AssertionFailedException 35 | */ 36 | public function it_raises_an_event_called_member_signed_up_when_the_method_sign_up_is_called(): void 37 | { 38 | $faker = Factory::create(); 39 | 40 | $password = $faker->password(); 41 | 42 | $member = Member::signUp( 43 | id: Id::createFromString(value: $this->memberRepository->generateIdentity()), 44 | firstName: FirstName::createFromString(value: $faker->firstName()), 45 | lastName: LastName::createFromString(value: $faker->lastName()), 46 | emailAddress: EmailAddress::createFromString(value: $faker->unique()->freeEmail()), 47 | status: StatusName::PENDING, 48 | createdAt: $this->clock->now(), 49 | updatedAt: $this->clock->now(), 50 | password: Password::createFromString(value: $this->hasher->make(value: $password)) 51 | ); 52 | 53 | $events = $member->releaseEvents(); 54 | 55 | $this->assertCount(expectedCount: 1, haystack: $events); 56 | $this->assertInstanceOf(expected: MemberSignedUp::class, actual: $events[0]); 57 | $this->assertCount(expectedCount: 0, haystack: $member->releaseEvents()); 58 | $this->assertInstanceOf(expected: Member::class, actual: $member); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Tests/Infrastructure/Members/SignUpMemberControllerTest.php: -------------------------------------------------------------------------------- 1 | app = $this->createApplication(); 32 | $this->app->bind(abstract: Mailer::class, concrete: MailFake::class); 33 | $this->app->bind(abstract: Queue::class, concrete: fn () => new QueueFake(app: $this->app)); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function a_member_can_sign_up_for_a_new_account(): void 40 | { 41 | $member = Member::factory()->make(); 42 | 43 | $response = $this->postJson( 44 | uri: $this->app->make( 45 | abstract: UrlGenerator::class 46 | )->route( 47 | name: 'api.v1.member-sign-ups', 48 | parameters: array_merge($member->toArray(), ['password_confirmation' => $member->password]) 49 | ) 50 | ); 51 | 52 | $this->assertDatabaseCount(table: 'users', count: 1); 53 | 54 | $this->assertDatabaseHas(table: 'users', data: Arr::except(array: $member->toArray(), keys: ['password', 'password_confirmation'])); 55 | 56 | $response->assertStatus(status: Response::HTTP_CREATED); 57 | } 58 | 59 | /** 60 | * @test 61 | * @dataProvider signUpMemberDataProvider 62 | */ 63 | public function sign_up_member_validation_errors(string $field, mixed $value, string $errorField = ''): void 64 | { 65 | $member = Member::factory()->make(); 66 | 67 | $parameters = array_merge($member->toArray(), [$field => $value]); 68 | 69 | $response = $this->postJson(uri: $this->app->make(abstract: UrlGenerator::class)->route(name: 'api.v1.member-sign-ups', parameters: $parameters)); 70 | 71 | $this->assertDatabaseMissing(table: 'users', data: Arr::except(array: $member->toArray(), keys: ['password'])); 72 | 73 | $response->assertStatus(status: Response::HTTP_UNPROCESSABLE_ENTITY) 74 | ->assertJsonValidationErrors(errors: $errorField ?: $field); 75 | } 76 | 77 | public static function signUpMemberDataProvider(): array 78 | { 79 | return [ 80 | 'The first name field is required' => ['first_name', ''], 81 | 'The first name may not be greater than 100 characters' => [ 82 | 'first_name', 83 | str_repeat(string: 'a', times: 110), 84 | ], 85 | 'The last name field is required' => ['last_name', ''], 86 | 'The last name may not be greater than 50 characters' => [ 87 | 'last_name', 88 | str_repeat(string: 'a', times: 110), 89 | ], 90 | 'The email field is required' => ['email', ''], 91 | 'The email may not be greater than 200 characters' => [ 92 | 'email', 93 | str_repeat(string: 'a', times: 210), 94 | ], 95 | 'The email must be a valid email address (format)' => ['email', 'invalidemailaddress'], 96 | 'The email must be a valid email address (domain)' => ['email', 'test@invaliddomainthatdoesnotexist.com'], 97 | 'The email must be a valid email address (RFC)' => ['email', 'p[][;lp@example.com'], 98 | 'The email has already been taken' => ['email', 'test@example.com'], 99 | 'The password must be at least 8 characters' => ['password', 'secret'], 100 | 'The password confirmation does not match' => ['password_confirmation', 'Secret111', 'password'], 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/Tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class {{ factory }}Factory extends Factory 13 | { 14 | /** 15 | * Define the model's default state. 16 | * 17 | * @return array 18 | */ 19 | public function definition() 20 | { 21 | return [ 22 | // 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/stubs/job.queued.stub: -------------------------------------------------------------------------------- 1 | markdown('{{ view }}'); 46 | } 47 | 48 | /** 49 | * Get the array representation of the notification. 50 | * 51 | * @param mixed $notifiable 52 | * @return array 53 | */ 54 | public function toArray($notifiable) 55 | { 56 | return [ 57 | // 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/stubs/middleware.stub: -------------------------------------------------------------------------------- 1 | snowflake()->primary(); 20 | $table->timestamp(column: 'created_at')->nullable(); 21 | $table->timestamp(column: 'updated_at')->nullable(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('{{ table }}'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/stubs/migration.stub: -------------------------------------------------------------------------------- 1 | line('The introduction to the notification.') 47 | ->action('Notification Action', url('/')) 48 | ->line('Thank you for using our application!'); 49 | } 50 | 51 | /** 52 | * Get the array representation of the notification. 53 | * 54 | * @param mixed $notifiable 55 | * @return array 56 | */ 57 | public function toArray($notifiable) 58 | { 59 | return [ 60 | // 61 | ]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/stubs/observer.plain.stub: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | public function rules() 27 | { 28 | return [ 29 | // 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/stubs/resource-collection.stub: -------------------------------------------------------------------------------- 1 | get('/'); 21 | 22 | $response->assertStatus(200); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/stubs/test.unit.stub: -------------------------------------------------------------------------------- 1 | assertTrue(true); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /App/Infrastructure/Laravel/stubs/view-component.stub: -------------------------------------------------------------------------------- 1 | $members 18 | */ 19 | public function __construct(private array $members = []) 20 | { 21 | } 22 | 23 | public function generateIdentity(): string 24 | { 25 | return (new Snowflake())->id(); 26 | } 27 | 28 | /** 29 | * @throws MemberWasNotFound 30 | * @throws AssertionFailedException 31 | */ 32 | public function findByEmailAddress(string $emailAddress): MemberReadModel 33 | { 34 | foreach ($this->members as $member) { 35 | if ($member['email'] === $emailAddress) { 36 | return MemberReadModel::createFromArray([ 37 | 'id' => $member['id'], 38 | 'firstName' => $member['first_name'], 39 | 'lastName' => $member['last_name'], 40 | 'email' => $member['email'], 41 | 'status' => $member['status'], 42 | 'createdAt' => $member['created_at'], 43 | 'updatedAt' => $member['updated_at'], 44 | ]); 45 | } 46 | } 47 | 48 | throw new MemberWasNotFound(); 49 | } 50 | 51 | public function existsByEmailAddress(string $emailAddress): bool 52 | { 53 | foreach ($this->members as $member) { 54 | if ($member['email'] === $emailAddress) { 55 | return true; 56 | } 57 | } 58 | 59 | return false; 60 | } 61 | 62 | public function save(Member $member): void 63 | { 64 | $this->members[] = $member->mapForPersistence(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /App/Infrastructure/Members/EloquentMemberRepository.php: -------------------------------------------------------------------------------- 1 | id(); 24 | } 25 | 26 | /** 27 | * @throws MemberWasNotFound|AssertionFailedException 28 | */ 29 | public function findByEmailAddress(string $emailAddress, array $columns = self::DEFAULT_COLUMNS): MemberReadModel 30 | { 31 | $member = $this->member->newQuery() 32 | ->select(columns: $columns) 33 | ->where(column: 'email', operator: '=', value: $emailAddress) 34 | ->first(); 35 | 36 | if (! $member) { 37 | throw new MemberWasNotFound(); 38 | } 39 | 40 | return MemberReadModel::createFromEloquentModel(member: $member); 41 | } 42 | 43 | public function existsByEmailAddress(string $emailAddress): bool 44 | { 45 | return $this->member->newQuery() 46 | ->where(column: 'email', operator: '=', value: $emailAddress) 47 | ->exists(); 48 | } 49 | 50 | public function save(Member $member): void 51 | { 52 | $this->member->newQuery()->create(attributes: $member->mapForPersistence()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /App/Infrastructure/Members/Member.php: -------------------------------------------------------------------------------- 1 | 54 | */ 55 | protected $casts = [ 56 | 'id' => SnowflakeCast::class, 57 | 'first_name' => 'string', 58 | 'last_name' => 'string', 59 | 'email' => 'string', 60 | 'status' => StatusName::class, 61 | 'password' => 'string', 62 | 'remember_token' => 'string', 63 | 'created_at' => 'immutable_datetime', 64 | 'updated_at' => 'immutable_datetime', 65 | 'email_verified_at' => 'immutable_datetime', 66 | ]; 67 | } 68 | -------------------------------------------------------------------------------- /App/Infrastructure/Members/MemberActivationEmail.php: -------------------------------------------------------------------------------- 1 | $this->firstName->getValue().' '.$this->lastName->getValue(), 52 | 'activationUrl' => $this->configurationRepository->get(key: 'members.activationUrl'), 53 | 'appName' => $this->configurationRepository->get(key: 'app.name'), 54 | ], 55 | ); 56 | } 57 | 58 | /** 59 | * Get the attachments for the message. 60 | */ 61 | public function attachments(): array 62 | { 63 | return []; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /App/Infrastructure/Members/MemberFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class MemberFactory extends Factory 14 | { 15 | protected $model = Member::class; 16 | 17 | /** 18 | * Define the model's default state. 19 | * 20 | * @return array 21 | */ 22 | public function definition(): array 23 | { 24 | return [ 25 | 'first_name' => $this->faker->firstName(), 26 | 'last_name' => $this->faker->lastName(), 27 | 'email' => $this->faker->unique()->freeEmail(), 28 | 'password' => $this->faker->password(minLength: 8), 29 | 'status' => StatusName::PENDING->value, 30 | 'created_at' => Carbon::now(), 31 | 'updated_at' => Carbon::now(), 32 | 'email_verified_at' => null, 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /App/Infrastructure/Members/SignUpMemberController.php: -------------------------------------------------------------------------------- 1 | commandBusDispatcher->dispatch(command: new SignUpMember( 23 | firstName: $signUpMemberFormRequest->input(key: 'first_name'), 24 | lastName: $signUpMemberFormRequest->input(key: 'last_name'), 25 | emailAddress: $signUpMemberFormRequest->input(key: 'email'), 26 | password: $signUpMemberFormRequest->input(key: 'password') 27 | )); 28 | 29 | return new Response(content: null, status: SymfonyResponse::HTTP_CREATED); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /App/Infrastructure/Members/SignUpMemberFormRequest.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | 'first_name' => ['required', 'string', 'max:50'], 26 | 'last_name' => ['required', 'string', 'max:50'], 27 | 'email' => ['required', 'string', 'email:dns,rfc', 'max:255'], 28 | 'password' => ['required', 'string', 'min:8', 'confirmed'], 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /App/Infrastructure/Members/activation.blade.php: -------------------------------------------------------------------------------- 1 | 2 | # Activate Your Membership 3 | 4 | Hi {{ $fullName }}, 5 | Your membership sign up is almost complete. Please confirm your membership by clicking on the button below. 6 | 7 | 8 | Activate Membership 9 | 10 | 11 | Thank you,
12 | {{ $appName }} 13 |
14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 LearnHub.dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel API Starter Kit 2 | 3 | An opinionated Software-as-a-Service Laravel API Starter Kit for long-term projects inspired by the tactical patterns from Domain-Driven Design and the folder structure from Clean Architecture. 4 | 5 | Things to do before releasing the v1 officially: 6 | 7 | - [x] Prepare the initial folder structure 8 | - [ ] Review the folder structure to see if it can be improved (potentially split the code to clean architecture and vertical slices architecture in different branches) 9 | - [ ] Implement authentication for both Sanctum single page app and mobile versions 10 | - [x] Implement member signups 11 | - [ ] Implement member activation 12 | - [ ] Implement forgot password 13 | - [ ] Implement password confirmation 14 | - [ ] Implement teams 15 | - [ ] Implement 2-factor authentication 16 | - [ ] Implement profile management 17 | - [ ] Implement a caching strategy 18 | - [x] Set up testing 19 | - [ ] Set up mutation testing (infection PHP) 20 | - [ ] Set up PHPStan 21 | - [ ] Set up Deptrac 22 | - [ ] Set up PHPArkitect 23 | - [ ] Set up GitHub Actions to run all of the code quality tools automatically on new pull requests 24 | - [ ] Update the documentation to add information about the package, what it is, why it's being built, how to use it and what are the rules that need to be followed in order to make the package worth using as a starting point for long-term Laravel apps 25 | - [ ] Think of new features to add for v2 and beyond 26 | - [ ] Review the tests and see if they need to be updated or more tests need to be added 27 | - [ ] Update few of the artisan commands to support the custom structure 28 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(abstract: Kernel::class); 38 | 39 | $status = $kernel->handle( 40 | input: $input = new ArgvInput, 41 | output: new ConsoleOutput 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Shutdown The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | Once Artisan has finished running, we will fire off the shutdown events 50 | | so that any final work may be done by the application before we shut 51 | | down the process. This is the last thing to happen to the request. 52 | | 53 | */ 54 | 55 | $kernel->terminate(input: $input, status: $status); 56 | 57 | exit($status); 58 | -------------------------------------------------------------------------------- /behat.yaml: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | application: 4 | paths: [] 5 | contexts: [] 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learnhubdev/laravel-api-starter-kit", 3 | "type": "project", 4 | "version": "0.2.5", 5 | "description": "A Laravel API Starter Kit for long-term projects inspired by the tactical patterns of Domain-Driven Design", 6 | "keywords": ["framework", "laravel", "starter kit"], 7 | "license": "MIT", 8 | "require": { 9 | "php": "^8.2.0", 10 | "ext-pdo": "*", 11 | "beberlei/assert": "^3.3", 12 | "caneara/snowflake": "^2.0", 13 | "doctrine/dbal": "^3.6", 14 | "guzzlehttp/guzzle": "^7.5", 15 | "laravel/framework": "^v10.0.0", 16 | "laravel/sanctum": "^3.2", 17 | "laravel/tinker": "^2.7", 18 | "spatie/laravel-route-attributes": "^1.18", 19 | "symfony/clock": "^6.2@beta" 20 | }, 21 | "require-dev": { 22 | "barryvdh/laravel-ide-helper": "^2.12", 23 | "behat/behat": "^3.12", 24 | "brianium/paratest": "^7.0", 25 | "driftingly/rector-laravel": "*", 26 | "fakerphp/faker": "^1.9.1", 27 | "infection/infection": "^0.26.15", 28 | "kubawerlos/php-cs-fixer-custom-fixers": "^3.14", 29 | "laravel/pint": "^1.0", 30 | "laravel/sail": "^1.0.1", 31 | "mockery/mockery": "^1.4.4", 32 | "mortexa/laravel-arkitect": "^0.3.3", 33 | "nunomaduro/collision": "^7.0", 34 | "nunomaduro/larastan": "^2.0", 35 | "phparkitect/phparkitect": "^0.2.32", 36 | "phpunit/phpunit": "^10.0.0", 37 | "qossmic/deptrac-shim": "*", 38 | "rector/rector": "^0.14.6", 39 | "spatie/laravel-ignition": "^2.0" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "App\\": "App/", 44 | "Laravel\\": "App/Infrastructure/Laravel/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Tests\\": "App/Infrastructure/Laravel/Tests/" 50 | } 51 | }, 52 | "scripts": { 53 | "test:phpunit": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --testdox", 54 | "test:behat": "./vendor/bin/behat", 55 | "test": [ 56 | "@test:phpunit" 57 | ], 58 | "fix": "./vendor/bin/pint", 59 | "post-autoload-dump": [ 60 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 61 | "@php artisan package:discover --ansi" 62 | ], 63 | "post-update-cmd": [ 64 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 65 | ], 66 | "post-root-package-install": [ 67 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 68 | ], 69 | "post-create-project-cmd": [ 70 | "@php artisan key:generate --ansi" 71 | ] 72 | }, 73 | "extra": { 74 | "laravel": { 75 | "dont-discover": [] 76 | } 77 | }, 78 | "config": { 79 | "optimize-autoloader": true, 80 | "preferred-install": "dist", 81 | "sort-packages": true, 82 | "allow-plugins": { 83 | "pestphp/pest-plugin": true, 84 | "infection/extension-installer": true 85 | } 86 | }, 87 | "minimum-stability": "stable", 88 | "prefer-stable": true 89 | } 90 | -------------------------------------------------------------------------------- /deptrac.yaml: -------------------------------------------------------------------------------- 1 | # deptrac.yaml 2 | deptrac: 3 | paths: 4 | - ./App 5 | exclude_files: 6 | - '#.*test.*#' 7 | layers: 8 | - 9 | name: Controller 10 | collectors: 11 | - 12 | type: className 13 | value: .*Controller.* 14 | - 15 | name: Repository 16 | collectors: 17 | - 18 | type: className 19 | value: .*Repository.* 20 | - 21 | name: Service 22 | collectors: 23 | - 24 | type: className 25 | value: .*Service.* 26 | ruleset: 27 | Controller: 28 | - Service 29 | Service: 30 | - Repository 31 | Repository: ~ 32 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # For more information: https://laravel.com/docs/sail 2 | version: '3' 3 | services: 4 | php: 5 | build: 6 | context: ./vendor/laravel/sail/runtimes/8.2 7 | dockerfile: Dockerfile 8 | args: 9 | WWWGROUP: '${WWWGROUP}' 10 | image: sail-8.2/app 11 | extra_hosts: 12 | - 'host.docker.internal:host-gateway' 13 | ports: 14 | - '${APP_PORT:-80}:80' 15 | environment: 16 | WWWUSER: '${WWWUSER}' 17 | LARAVEL_SAIL: 1 18 | XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' 19 | XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' 20 | volumes: 21 | - '.:/var/www/html' 22 | networks: 23 | - sail 24 | depends_on: 25 | - redis 26 | - mysql 27 | - mailpit 28 | redis: 29 | image: 'redis:alpine' 30 | ports: 31 | - '${FORWARD_REDIS_PORT:-6379}:6379' 32 | volumes: 33 | - 'sail-redis:/data' 34 | networks: 35 | - sail 36 | healthcheck: 37 | test: ["CMD", "redis-cli", "ping"] 38 | retries: 3 39 | timeout: 5s 40 | mysql: 41 | image: 'mysql/mysql-server:8.0' 42 | ports: 43 | - '${FORWARD_DB_PORT:-3306}:3306' 44 | environment: 45 | MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}' 46 | MYSQL_ROOT_HOST: "%" 47 | MYSQL_DATABASE: '${DB_DATABASE}' 48 | MYSQL_USER: '${DB_USERNAME}' 49 | MYSQL_PASSWORD: '${DB_PASSWORD}' 50 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 51 | volumes: 52 | - 'sail-mysql:/var/lib/mysql' 53 | - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh' 54 | networks: 55 | - sail 56 | healthcheck: 57 | test: ["CMD", "mysqladmin", "ping", "-p${DB_PASSWORD}"] 58 | retries: 3 59 | timeout: 5s 60 | 61 | mailpit: 62 | image: 'axllent/mailpit:latest' 63 | ports: 64 | - '${FORWARD_MAILPIT_PORT:-1025}:1025' 65 | - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025' 66 | networks: 67 | - sail 68 | 69 | networks: 70 | sail: 71 | driver: bridge 72 | volumes: 73 | sail-redis: 74 | driver: local 75 | sail-mysql: 76 | driver: local 77 | -------------------------------------------------------------------------------- /docker/7.4/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | LABEL maintainer="Taylor Otwell" 4 | 5 | ARG WWWGROUP 6 | ARG NODE_VERSION=16 7 | ARG POSTGRES_VERSION=13 8 | 9 | WORKDIR /var/www/html 10 | 11 | ENV DEBIAN_FRONTEND noninteractive 12 | ENV TZ=UTC 13 | 14 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 15 | 16 | RUN apt-get update \ 17 | && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \ 18 | && mkdir -p ~/.gnupg \ 19 | && chmod 600 ~/.gnupg \ 20 | && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \ 21 | && echo "keyserver hkp://keyserver.ubuntu.com:80" >> ~/.gnupg/dirmngr.conf \ 22 | && gpg --recv-key 0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c \ 23 | && gpg --export 0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c > /usr/share/keyrings/ppa_ondrej_php.gpg \ 24 | && echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu focal main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ 25 | && apt-get update \ 26 | && apt-get install -y php7.4-cli php7.4-dev \ 27 | php7.4-pgsql php7.4-sqlite3 php7.4-gd \ 28 | php7.4-curl php7.4-memcached \ 29 | php7.4-imap php7.4-mysql php7.4-mbstring \ 30 | php7.4-xml php7.4-zip php7.4-bcmath php7.4-soap \ 31 | php7.4-intl php7.4-readline php7.4-pcov \ 32 | php7.4-msgpack php7.4-igbinary php7.4-ldap \ 33 | php7.4-redis php7.4-xdebug \ 34 | && php -r "readfile('https://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ 35 | && curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ 36 | && apt-get install -y nodejs \ 37 | && npm install -g npm \ 38 | && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null \ 39 | && echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ 40 | && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \ 41 | && echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt focal-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ 42 | && apt-get update \ 43 | && apt-get install -y yarn \ 44 | && apt-get install -y mysql-client \ 45 | && apt-get install -y postgresql-client-$POSTGRES_VERSION \ 46 | && apt-get -y autoremove \ 47 | && apt-get clean \ 48 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 49 | 50 | RUN setcap "cap_net_bind_service=+ep" /usr/bin/php7.4 51 | 52 | RUN groupadd --force -g $WWWGROUP sail 53 | RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail 54 | 55 | COPY start-container /usr/local/bin/start-container 56 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 57 | COPY php.ini /etc/php/7.4/cli/conf.d/99-sail.ini 58 | RUN chmod +x /usr/local/bin/start-container 59 | 60 | EXPOSE 8000 61 | 62 | ENTRYPOINT ["start-container"] 63 | -------------------------------------------------------------------------------- /docker/7.4/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 100M 3 | upload_max_filesize = 100M 4 | variables_order = EGPCS 5 | -------------------------------------------------------------------------------- /docker/7.4/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! -z "$WWWUSER" ]; then 4 | usermod -u $WWWUSER sail 5 | fi 6 | 7 | if [ ! -d /.composer ]; then 8 | mkdir /.composer 9 | fi 10 | 11 | chmod -R ugo+rw /.composer 12 | 13 | if [ $# -gt 0 ]; then 14 | exec gosu $WWWUSER "$@" 15 | else 16 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf 17 | fi 18 | -------------------------------------------------------------------------------- /docker/7.4/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:php] 8 | command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 9 | user=sail 10 | environment=LARAVEL_SAIL="1" 11 | stdout_logfile=/dev/stdout 12 | stdout_logfile_maxbytes=0 13 | stderr_logfile=/dev/stderr 14 | stderr_logfile_maxbytes=0 15 | -------------------------------------------------------------------------------- /docker/8.0/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | LABEL maintainer="Taylor Otwell" 4 | 5 | ARG WWWGROUP 6 | ARG NODE_VERSION=16 7 | ARG POSTGRES_VERSION=13 8 | 9 | WORKDIR /var/www/html 10 | 11 | ENV DEBIAN_FRONTEND noninteractive 12 | ENV TZ=UTC 13 | 14 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 15 | 16 | RUN apt-get update \ 17 | && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \ 18 | && mkdir -p ~/.gnupg \ 19 | && chmod 600 ~/.gnupg \ 20 | && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \ 21 | && echo "keyserver hkp://keyserver.ubuntu.com:80" >> ~/.gnupg/dirmngr.conf \ 22 | && gpg --recv-key 0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c \ 23 | && gpg --export 0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c > /usr/share/keyrings/ppa_ondrej_php.gpg \ 24 | && echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu focal main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ 25 | && apt-get update \ 26 | && apt-get install -y php8.0-cli php8.0-dev \ 27 | php8.0-pgsql php8.0-sqlite3 php8.0-gd \ 28 | php8.0-curl php8.0-memcached \ 29 | php8.0-imap php8.0-mysql php8.0-mbstring \ 30 | php8.0-xml php8.0-zip php8.0-bcmath php8.0-soap \ 31 | php8.0-intl php8.0-readline php8.0-pcov \ 32 | php8.0-msgpack php8.0-igbinary php8.0-ldap \ 33 | php8.0-redis php8.0-swoole php8.0-xdebug \ 34 | && php -r "readfile('https://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ 35 | && curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ 36 | && apt-get install -y nodejs \ 37 | && npm install -g npm \ 38 | && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null \ 39 | && echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ 40 | && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \ 41 | && echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt focal-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ 42 | && apt-get update \ 43 | && apt-get install -y yarn \ 44 | && apt-get install -y mysql-client \ 45 | && apt-get install -y postgresql-client-$POSTGRES_VERSION \ 46 | && apt-get -y autoremove \ 47 | && apt-get clean \ 48 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 49 | 50 | RUN update-alternatives --set php /usr/bin/php8.0 51 | 52 | RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.0 53 | 54 | RUN groupadd --force -g $WWWGROUP sail 55 | RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail 56 | 57 | COPY start-container /usr/local/bin/start-container 58 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 59 | COPY php.ini /etc/php/8.0/cli/conf.d/99-sail.ini 60 | RUN chmod +x /usr/local/bin/start-container 61 | 62 | EXPOSE 8000 63 | 64 | ENTRYPOINT ["start-container"] 65 | -------------------------------------------------------------------------------- /docker/8.0/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 100M 3 | upload_max_filesize = 100M 4 | variables_order = EGPCS 5 | -------------------------------------------------------------------------------- /docker/8.0/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! -z "$WWWUSER" ]; then 4 | usermod -u $WWWUSER sail 5 | fi 6 | 7 | if [ ! -d /.composer ]; then 8 | mkdir /.composer 9 | fi 10 | 11 | chmod -R ugo+rw /.composer 12 | 13 | if [ $# -gt 0 ]; then 14 | exec gosu $WWWUSER "$@" 15 | else 16 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf 17 | fi 18 | -------------------------------------------------------------------------------- /docker/8.0/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:php] 8 | command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 9 | user=sail 10 | environment=LARAVEL_SAIL="1" 11 | stdout_logfile=/dev/stdout 12 | stdout_logfile_maxbytes=0 13 | stderr_logfile=/dev/stderr 14 | stderr_logfile_maxbytes=0 15 | -------------------------------------------------------------------------------- /docker/8.1/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | LABEL maintainer="Taylor Otwell" 4 | 5 | ARG WWWGROUP 6 | ARG NODE_VERSION=16 7 | ARG POSTGRES_VERSION=14 8 | 9 | WORKDIR /var/www/html 10 | 11 | ENV DEBIAN_FRONTEND noninteractive 12 | ENV TZ=UTC 13 | 14 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 15 | 16 | RUN apt-get update \ 17 | && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \ 18 | && mkdir -p ~/.gnupg \ 19 | && chmod 600 ~/.gnupg \ 20 | && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \ 21 | && echo "keyserver hkp://keyserver.ubuntu.com:80" >> ~/.gnupg/dirmngr.conf \ 22 | && gpg --recv-key 0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c \ 23 | && gpg --export 0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c > /usr/share/keyrings/ppa_ondrej_php.gpg \ 24 | && echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ 25 | && apt-get update \ 26 | && apt-get install -y php8.1-cli php8.1-dev \ 27 | php8.1-pgsql php8.1-sqlite3 php8.1-gd \ 28 | php8.1-curl \ 29 | php8.1-imap php8.1-mysql php8.1-mbstring \ 30 | php8.1-xml php8.1-zip php8.1-bcmath php8.1-soap \ 31 | php8.1-intl php8.1-readline \ 32 | php8.1-ldap \ 33 | php8.1-msgpack php8.1-igbinary php8.1-redis php8.1-swoole \ 34 | php8.1-memcached php8.1-pcov php8.1-xdebug \ 35 | && php -r "readfile('https://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ 36 | && curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ 37 | && apt-get install -y nodejs \ 38 | && npm install -g npm \ 39 | && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarn.gpg >/dev/null \ 40 | && echo "deb [signed-by=/usr/share/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ 41 | && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \ 42 | && echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ 43 | && apt-get update \ 44 | && apt-get install -y yarn \ 45 | && apt-get install -y mysql-client \ 46 | && apt-get install -y postgresql-client-$POSTGRES_VERSION \ 47 | && apt-get -y autoremove \ 48 | && apt-get clean \ 49 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 50 | 51 | RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.1 52 | 53 | RUN groupadd --force -g $WWWGROUP sail 54 | RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail 55 | 56 | COPY start-container /usr/local/bin/start-container 57 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 58 | COPY php.ini /etc/php/8.1/cli/conf.d/99-sail.ini 59 | RUN chmod +x /usr/local/bin/start-container 60 | 61 | EXPOSE 8000 62 | 63 | ENTRYPOINT ["start-container"] 64 | -------------------------------------------------------------------------------- /docker/8.1/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 100M 3 | upload_max_filesize = 100M 4 | variables_order = EGPCS 5 | -------------------------------------------------------------------------------- /docker/8.1/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! -z "$WWWUSER" ]; then 4 | usermod -u $WWWUSER sail 5 | fi 6 | 7 | if [ ! -d /.composer ]; then 8 | mkdir /.composer 9 | fi 10 | 11 | chmod -R ugo+rw /.composer 12 | 13 | if [ $# -gt 0 ]; then 14 | exec gosu $WWWUSER "$@" 15 | else 16 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf 17 | fi 18 | -------------------------------------------------------------------------------- /docker/8.1/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:php] 8 | command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 9 | user=sail 10 | environment=LARAVEL_SAIL="1" 11 | stdout_logfile=/dev/stdout 12 | stdout_logfile_maxbytes=0 13 | stderr_logfile=/dev/stderr 14 | stderr_logfile_maxbytes=0 15 | -------------------------------------------------------------------------------- /docker/8.2/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | LABEL maintainer="Taylor Otwell" 4 | 5 | ARG WWWGROUP 6 | ARG NODE_VERSION=16 7 | ARG POSTGRES_VERSION=14 8 | 9 | WORKDIR /var/www/html 10 | 11 | ENV DEBIAN_FRONTEND noninteractive 12 | ENV TZ=UTC 13 | 14 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 15 | 16 | RUN apt-get update \ 17 | && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 \ 18 | && mkdir -p ~/.gnupg \ 19 | && chmod 600 ~/.gnupg \ 20 | && echo "disable-ipv6" >> ~/.gnupg/dirmngr.conf \ 21 | && echo "keyserver hkp://keyserver.ubuntu.com:80" >> ~/.gnupg/dirmngr.conf \ 22 | && gpg --recv-key 0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c \ 23 | && gpg --export 0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c > /usr/share/keyrings/ppa_ondrej_php.gpg \ 24 | && echo "deb [signed-by=/usr/share/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ 25 | && apt-get update \ 26 | && apt-get install -y php8.2-cli php8.2-dev \ 27 | php8.2-pgsql php8.2-sqlite3 php8.2-gd \ 28 | php8.2-curl \ 29 | php8.2-imap php8.2-mysql php8.2-mbstring \ 30 | php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \ 31 | php8.2-intl php8.2-readline \ 32 | php8.2-ldap \ 33 | # php8.2-msgpack php8.2-igbinary php8.2-redis php8.2-swoole \ 34 | # php8.2-memcached php8.2-pcov php8.2-xdebug \ 35 | && php -r "readfile('https://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --filename=composer \ 36 | && curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ 37 | && apt-get install -y nodejs \ 38 | && npm install -g npm \ 39 | && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarn.gpg >/dev/null \ 40 | && echo "deb [signed-by=/usr/share/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ 41 | && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/pgdg.gpg >/dev/null \ 42 | && echo "deb [signed-by=/usr/share/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ 43 | && apt-get update \ 44 | && apt-get install -y yarn \ 45 | && apt-get install -y mysql-client \ 46 | && apt-get install -y postgresql-client-$POSTGRES_VERSION \ 47 | && apt-get -y autoremove \ 48 | && apt-get clean \ 49 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 50 | 51 | RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.2 52 | 53 | RUN groupadd --force -g $WWWGROUP sail 54 | RUN useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail 55 | 56 | COPY start-container /usr/local/bin/start-container 57 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 58 | COPY php.ini /etc/php/8.2/cli/conf.d/99-sail.ini 59 | RUN chmod +x /usr/local/bin/start-container 60 | 61 | EXPOSE 8000 62 | 63 | ENTRYPOINT ["start-container"] 64 | -------------------------------------------------------------------------------- /docker/8.2/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 100M 3 | upload_max_filesize = 100M 4 | variables_order = EGPCS 5 | -------------------------------------------------------------------------------- /docker/8.2/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! -z "$WWWUSER" ]; then 4 | usermod -u $WWWUSER sail 5 | fi 6 | 7 | if [ ! -d /.composer ]; then 8 | mkdir /.composer 9 | fi 10 | 11 | chmod -R ugo+rw /.composer 12 | 13 | if [ $# -gt 0 ]; then 14 | exec gosu $WWWUSER "$@" 15 | else 16 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf 17 | fi 18 | -------------------------------------------------------------------------------- /docker/8.2/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:php] 8 | command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 9 | user=sail 10 | environment=LARAVEL_SAIL="1" 11 | stdout_logfile=/dev/stdout 12 | stdout_logfile_maxbytes=0 13 | stderr_logfile=/dev/stderr 14 | stderr_logfile_maxbytes=0 15 | -------------------------------------------------------------------------------- /infection.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "vendor/infection/infection/resources/schema.json", 3 | "source": { 4 | "directories": [ 5 | "App/" 6 | ], 7 | "excludes": ["App/Infrastructure/Laravel/Resources/Views/*.blade.php"] 8 | }, 9 | "mutators": { 10 | "@default": true 11 | }, 12 | 13 | } 14 | -------------------------------------------------------------------------------- /phparkitect.php: -------------------------------------------------------------------------------- 1 | component('Controller')->definedBy('App\Controller\*') 17 | ->component('Service')->definedBy('App\Service\*') 18 | ->component('Repository')->definedBy('App\Repository\*') 19 | ->component('Entity')->definedBy('App\Entity\*') 20 | 21 | ->where('Controller')->mayDependOnComponents('Service', 'Entity') 22 | ->where('Service')->mayDependOnComponents('Repository', 'Entity') 23 | ->where('Repository')->mayDependOnComponents('Entity') 24 | ->where('Entity')->shouldNotDependOnAnyComponent() 25 | 26 | ->rules(); 27 | 28 | $serviceNamingRule = Rule::allClasses() 29 | ->that(new ResideInOneOfTheseNamespaces('App\Service')) 30 | ->should(new HaveNameMatching('*Service')) 31 | ->because('we want uniform naming for services'); 32 | 33 | $repositoryNamingRule = Rule::allClasses() 34 | ->that(new ResideInOneOfTheseNamespaces('App\Repository')) 35 | ->should(new HaveNameMatching('*Repository')) 36 | ->because('we want uniform naming for repositories'); 37 | 38 | $config->add($classSet, $serviceNamingRule, $repositoryNamingRule, ...$layeredArchitectureRules); 39 | }; 40 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | App/Infrastructure/Laravel/Tests/Domain 11 | App/Infrastructure/Laravel/Tests/Application 12 | 13 | 14 | App/Infrastructure/Laravel/Tests/Infrastructure 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no_superfluous_phpdoc_tags": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/learnhubdev/laravel-api-starter-kit/bb9d0ffcba6555df9c95c6c47e9db1fc4a06537e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(abstract: Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | request: $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate(request: $request, response: $response); 56 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 12 | __DIR__.'/App', 13 | ]); 14 | 15 | // register a single rule 16 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 17 | 18 | // define sets of rules 19 | $rectorConfig->sets([ 20 | // LevelSetList::UP_TO_PHP_82, 21 | PHPUnitLevelSetList::UP_TO_PHPUNIT_100 22 | ]); 23 | }; 24 | --------------------------------------------------------------------------------