From 82df1b08370cb43612ecd7fe7309c54e6234ccf2 Mon Sep 17 00:00:00 2001 From: Adam Lee Date: Sun, 11 Aug 2024 15:02:25 +0100 Subject: [PATCH] fix: Remove sushi dependency to avoid pdo requirement include as trait --- composer.json | 1 - src/Concerns/InteractsWithSushi.php | 250 ++++++++++++++++++++++++++++ src/Models/Endpoint.php | 4 +- src/Models/TestCoverage.php | 4 +- src/Models/TestReport.php | 4 +- 5 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 src/Concerns/InteractsWithSushi.php diff --git a/composer.json b/composer.json index 6c12e92..11856ff 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,6 @@ ], "require": { "php": "^8.2", - "calebporzio/sushi": "^2.5", "knuckleswtf/scribe": "^4.35", "laravel/framework": "^10|^11", "laravel/prompts": "^0.1.16", diff --git a/src/Concerns/InteractsWithSushi.php b/src/Concerns/InteractsWithSushi.php new file mode 100644 index 0000000..06a97a8 --- /dev/null +++ b/src/Concerns/InteractsWithSushi.php @@ -0,0 +1,250 @@ +sushiCachePath(); + $dataPath = $instance->sushiCacheReferencePath(); + + $states = [ + 'cache-file-found-and-up-to-date' => function () use ($cachePath): void { + static::setSqliteConnection($cachePath); + }, + 'cache-file-not-found-or-stale' => function () use ($cachePath, $dataPath, $instance): void { + static::cacheFileNotFoundOrStale($cachePath, $dataPath, $instance); + }, + 'no-caching-capabilities' => function () use ($instance): void { + static::setSqliteConnection(':memory:'); + + $instance->migrate(); + }, + ]; + + match (true) { + ! $instance->sushiShouldCache() => $states['no-caching-capabilities'](), + file_exists($cachePath) && filemtime($dataPath) <= filemtime($cachePath) => $states['cache-file-found-and-up-to-date'](), + file_exists($instance->sushiCacheDirectory()) && is_writable($instance->sushiCacheDirectory()) => $states['cache-file-not-found-or-stale'](), + default => $states['no-caching-capabilities'](), + }; + } + + public function getRows(): array + { + return $this->rows; + } + + public function getSchema(): array + { + return $this->schema ?? []; + } + + public function migrate(): void + { + $rows = $this->getRows(); + $tableName = $this->getTable(); + + if (count($rows) > 0) { + $this->createTable($tableName, $rows[0]); + } else { + $this->createTableWithNoData($tableName); + } + + foreach (array_chunk($rows, $this->getSushiInsertChunkSize()) ?? [] as $inserts) { + if ($inserts !== []) { + static::insert($inserts); + } + } + } + + public function createTable(string $tableName, $firstRow): void + { + $this->createTableSafely($tableName, function ($table) use ($firstRow): void { + // Add the "id" column if it doesn't already exist in the rows. + if ($this->incrementing && ! array_key_exists($this->primaryKey, $firstRow)) { + $table->increments($this->primaryKey); + } + + foreach ($firstRow as $column => $value) { + + $type = match (true) { + is_int($value) => 'integer', + is_numeric($value) => 'float', + is_string($value) => 'string', + $value instanceof DateTime => 'dateTime', + default => 'string', + }; + + if ($column === $this->primaryKey && $type === 'integer') { + $table->increments($this->primaryKey); + + continue; + } + + $schema = $this->getSchema(); + + $type = $schema[$column] ?? $type; + + $table->{$type}($column)->nullable(); + } + + if ($this->usesTimestamps() && (! in_array('updated_at', array_keys($firstRow)) || ! in_array('created_at', array_keys($firstRow)))) { + $table->timestamps(); + } + + $this->afterMigrate($table); + }); + } + + public function createTableWithNoData(string $tableName): void + { + $this->createTableSafely($tableName, function ($table): void { + $schema = $this->getSchema(); + + if ($this->incrementing && ! in_array($this->primaryKey, array_keys($schema))) { + $table->increments($this->primaryKey); + } + + foreach ($schema as $name => $type) { + if ($name === $this->primaryKey && $type === 'integer') { + $table->increments($this->primaryKey); + + continue; + } + + $table->{$type}($name)->nullable(); + } + if (! $this->usesTimestamps()) { + return; + } + if (in_array('updated_at', array_keys($schema)) && in_array('created_at', array_keys($schema))) { + return; + } + $table->timestamps(); + }); + } + + public function usesTimestamps(): bool + { + // Override the Laravel default value of $timestamps = true; Unless otherwise set. + return (new ReflectionClass($this))->getProperty('timestamps')->class === static::class && parent::usesTimestamps(); + } + + public function getSushiInsertChunkSize(): int + { + return $this->sushiInsertChunkSize ?? 100; + } + + public function getConnectionName(): string + { + return static::class; + } + + protected static function cacheFileNotFoundOrStale($cachePath, $dataPath, $instance): void + { + file_put_contents($cachePath, ''); + + static::setSqliteConnection($cachePath); + + $instance->migrate(); + + touch($cachePath, filemtime($dataPath)); + } + + protected static function setSqliteConnection($database): void + { + $config = [ + 'driver' => 'sqlite', + 'database' => $database, + ]; + + static::$sushiConnection = app(ConnectionFactory::class)->make($config); + + app('config')->set('database.connections.'.static::class, $config); + } + + protected function sushiCacheReferencePath(): string|false + { + return (new ReflectionClass(static::class))->getFileName(); + } + + protected function sushiShouldCache(): bool + { + return property_exists(static::class, 'rows'); + } + + protected function sushiCachePath(): string + { + return implode(DIRECTORY_SEPARATOR, [ + $this->sushiCacheDirectory(), + $this->sushiCacheFileName(), + ]); + } + + protected function sushiCacheFileName(): string + { + return 'sushi'.'-'.Str::kebab(str_replace('\\', '', static::class)).'.sqlite'; + } + + protected function sushiCacheDirectory(): string + { + return realpath(storage_path('framework/cache')); + } + + protected function newRelatedInstance($class): mixed + { + return tap(new $class, function ($instance): void { + if (! $instance->getConnectionName()) { + $instance->setConnection($this->getConnectionResolver()->getDefaultConnection()); + } + }); + } + + protected function afterMigrate(Blueprint $table): void + { + // + } + + protected function createTableSafely(string $tableName, Closure $callback): void + { + /** @var \Illuminate\Database\Schema\SQLiteBuilder $schemaBuilder */ + $schemaBuilder = static::resolveConnection()->getSchemaBuilder(); + + try { + $schemaBuilder->create($tableName, $callback); + } catch (QueryException $e) { + if (Str::contains($e->getMessage(), [ + 'already exists (SQL: create table', + sprintf('table "%s" already exists', $tableName), + ])) { + // This error can happen in rare circumstances due to a race condition. + // Concurrent requests may both see the necessary preconditions for + // the table creation, but only one can actually succeed. + return; + } + + throw $e; + } + } +} diff --git a/src/Models/Endpoint.php b/src/Models/Endpoint.php index ec38e8d..28fff2c 100644 --- a/src/Models/Endpoint.php +++ b/src/Models/Endpoint.php @@ -8,7 +8,7 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use InvalidArgumentException; -use Sushi\Sushi; +use XtendPackages\RESTPresenter\Concerns\InteractsWithSushi; /** * @property int $id @@ -20,7 +20,7 @@ */ class Endpoint extends Model { - use Sushi; + use InteractsWithSushi; /** * @return array diff --git a/src/Models/TestCoverage.php b/src/Models/TestCoverage.php index fe14d64..9ce71d6 100644 --- a/src/Models/TestCoverage.php +++ b/src/Models/TestCoverage.php @@ -5,11 +5,11 @@ namespace XtendPackages\RESTPresenter\Models; use Illuminate\Database\Eloquent\Model; -use Sushi\Sushi; +use XtendPackages\RESTPresenter\Concerns\InteractsWithSushi; class TestCoverage extends Model { - use Sushi; + use InteractsWithSushi; /** * @var array> diff --git a/src/Models/TestReport.php b/src/Models/TestReport.php index 15a0295..824d33b 100644 --- a/src/Models/TestReport.php +++ b/src/Models/TestReport.php @@ -5,11 +5,11 @@ namespace XtendPackages\RESTPresenter\Models; use Illuminate\Database\Eloquent\Model; -use Sushi\Sushi; +use XtendPackages\RESTPresenter\Concerns\InteractsWithSushi; class TestReport extends Model { - use Sushi; + use InteractsWithSushi; /** * @var array>