|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +namespace Tempest\Auth\Installer; |
| 6 | + |
| 7 | +use Tempest\Auth\OAuth\SupportedOAuthProvider; |
| 8 | +use Tempest\Core\PublishesFiles; |
| 9 | +use Tempest\Process\ProcessExecutor; |
| 10 | +use Tempest\Support\Filesystem\Exceptions\PathWasNotFound; |
| 11 | +use Tempest\Support\Filesystem\Exceptions\PathWasNotReadable; |
| 12 | +use Tempest\Support\Str\ImmutableString; |
| 13 | + |
| 14 | +use function Tempest\root_path; |
| 15 | +use function Tempest\src_path; |
| 16 | +use function Tempest\Support\arr; |
| 17 | +use function Tempest\Support\Filesystem\read_file; |
| 18 | +use function Tempest\Support\Namespace\to_fqcn; |
| 19 | +use function Tempest\Support\str; |
| 20 | + |
| 21 | +final class OAuthInstaller |
| 22 | +{ |
| 23 | + use PublishesFiles; |
| 24 | + |
| 25 | + public function __construct( |
| 26 | + private readonly ProcessExecutor $processExecutor, |
| 27 | + ) {} |
| 28 | + |
| 29 | + public function install(): void |
| 30 | + { |
| 31 | + $providers = $this->getProviders(); |
| 32 | + |
| 33 | + if (count($providers) === 0) { |
| 34 | + return; |
| 35 | + } |
| 36 | + |
| 37 | + $this->publishStubs(...$providers); |
| 38 | + |
| 39 | + if ($this->confirm('Would you like to add the OAuth config variables to your .env file?', default: true)) { |
| 40 | + $this->updateEnvFile(...$providers); |
| 41 | + } |
| 42 | + |
| 43 | + if ($this->confirm('Install composer dependencies?', default: true)) { |
| 44 | + $this->installComposerDependencies(...$providers); |
| 45 | + } |
| 46 | + |
| 47 | + $this->console->instructions([ |
| 48 | + sprintf('<strong>The selected OAuth %s installed in your project</strong>', count($providers) > 1 ? 'providers are' : 'provider is'), |
| 49 | + '', |
| 50 | + 'Next steps:', |
| 51 | + '1. Update the .env file with your OAuth credentials', |
| 52 | + '2. Implement the OAuth controller callback method', |
| 53 | + '3. Review and customize the published files if needed', |
| 54 | + '', |
| 55 | + '<strong>Published files</strong>', |
| 56 | + ...arr($this->publishedFiles)->map(fn (string $file) => '<style="fg-green">→</style> ' . $file), |
| 57 | + ]); |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * @return list<SupportedOAuthProvider> |
| 62 | + */ |
| 63 | + private function getProviders(): array |
| 64 | + { |
| 65 | + return $this->ask( |
| 66 | + question: 'Please choose an OAuth provider', |
| 67 | + options: SupportedOAuthProvider::cases(), |
| 68 | + multiple: true, |
| 69 | + ); |
| 70 | + } |
| 71 | + |
| 72 | + private function publishStubs(SupportedOAuthProvider ...$providers): void |
| 73 | + { |
| 74 | + foreach ($providers as $provider) { |
| 75 | + $this->publishController($provider); |
| 76 | + |
| 77 | + $this->publishConfig($provider); |
| 78 | + |
| 79 | + $this->publishImports(); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + private function publishConfig(SupportedOAuthProvider $provider): void |
| 84 | + { |
| 85 | + $name = strtolower($provider->name); |
| 86 | + $source = __DIR__ . "/../Installer/oauth/{$name}.config.stub.php"; |
| 87 | + |
| 88 | + $this->publish( |
| 89 | + source: $source, |
| 90 | + destination: src_path("Authentication/OAuth/{$name}.config.php"), |
| 91 | + ); |
| 92 | + } |
| 93 | + |
| 94 | + private function publishController(SupportedOAuthProvider $provider): void |
| 95 | + { |
| 96 | + $fileName = str($provider->value) |
| 97 | + ->classBasename() |
| 98 | + ->replace('Provider', '') |
| 99 | + ->append('Controller.php') |
| 100 | + ->toString(); |
| 101 | + |
| 102 | + $this->publish( |
| 103 | + source: __DIR__ . '/oauth/OAuthControllerStub.php', |
| 104 | + destination: src_path("Authentication/OAuth/{$fileName}"), |
| 105 | + callback: function (string $source, string $destination) use ($provider) { |
| 106 | + $providerFqcn = $provider::class; |
| 107 | + $name = strtolower($provider->name); |
| 108 | + $userModelFqcn = to_fqcn(src_path('Authentication/User.php'), root: root_path()); |
| 109 | + |
| 110 | + $this->update( |
| 111 | + path: $destination, |
| 112 | + callback: fn (ImmutableString $contents) => $contents->replace( |
| 113 | + search: [ |
| 114 | + "'tag_name'", |
| 115 | + 'redirect-route', |
| 116 | + 'callback-route', |
| 117 | + "'user-model-fqcn'", |
| 118 | + 'provider_db_column', |
| 119 | + ], |
| 120 | + replace: [ |
| 121 | + "\\{$providerFqcn}::{$provider->name}", |
| 122 | + "/auth/{$name}", |
| 123 | + "/auth/{$name}/callback", |
| 124 | + "\\{$userModelFqcn}::class", |
| 125 | + "{$name}_id", |
| 126 | + ], |
| 127 | + ), |
| 128 | + ); |
| 129 | + }, |
| 130 | + ); |
| 131 | + } |
| 132 | + |
| 133 | + private function installComposerDependencies(SupportedOAuthProvider ...$providers): void |
| 134 | + { |
| 135 | + $packages = arr($providers) |
| 136 | + ->map(fn (SupportedOAuthProvider $provider) => $provider->composerPackage()) |
| 137 | + ->filter(); |
| 138 | + |
| 139 | + if ($packages->isNotEmpty()) { |
| 140 | + $this->processExecutor->run("composer require {$packages->implode(' ')}"); |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + private function updateEnvFile(SupportedOAuthProvider ...$providers): void |
| 145 | + { |
| 146 | + arr($providers) |
| 147 | + ->map(fn (SupportedOAuthProvider $provider) => $this->extractSettings($provider)) |
| 148 | + ->filter() |
| 149 | + ->flatten() |
| 150 | + ->each(function (string $setting) { |
| 151 | + foreach (['.env', '.env.example'] as $envFile) { |
| 152 | + $this->update( |
| 153 | + path: root_path($envFile), |
| 154 | + callback: static fn (ImmutableString $contents): ImmutableString => $contents->contains($setting) |
| 155 | + ? $contents |
| 156 | + : $contents->append(PHP_EOL, "{$setting}="), |
| 157 | + ignoreNonExisting: true, |
| 158 | + ); |
| 159 | + } |
| 160 | + }); |
| 161 | + } |
| 162 | + |
| 163 | + private function extractSettings(SupportedOAuthProvider $provider): array |
| 164 | + { |
| 165 | + $name = strtolower($provider->name); |
| 166 | + $configPath = __DIR__ . "/../Installer/oauth/{$name}.config.stub.php"; |
| 167 | + |
| 168 | + try { |
| 169 | + return str(read_file($configPath)) |
| 170 | + ->matchAll("/env\('(OAUTH_[^']*)'/", matches: 1) |
| 171 | + ->map(fn (array $matches) => $matches[1] ?? null) |
| 172 | + ->filter() |
| 173 | + ->toArray(); |
| 174 | + } catch (PathWasNotFound|PathWasNotReadable) { |
| 175 | + return []; |
| 176 | + } |
| 177 | + } |
| 178 | +} |
0 commit comments