diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fad3ad32..6d72173b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,12 +8,13 @@ on: schedule: - cron: "0 0 * * *" -env: - COMPOSER_FLAGS: "--ansi --no-interaction --no-progress --prefer-dist -o -n" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: tests: - name: PHP ${{ matrix.php-version }} - L${{ matrix.laravel-version }} - ${{ matrix.os }} + name: P${{ matrix.php-version }} - L${{ matrix.laravel-version }} - ${{ matrix.stability }} - ${{ matrix.os }} strategy: fail-fast: false @@ -21,11 +22,13 @@ jobs: php-version: ['8.1', '8.2', '8.3'] laravel-version: [10, 11] os: [ubuntu-latest, windows-latest, macos-latest] - dependencies: [locked] + stability: [prefer-lowest, prefer-stable] experimental: [false] exclude: - laravel-version: 11 php-version: 8.1 + - laravel-version: 11 + stability: prefer-lowest runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} @@ -42,8 +45,8 @@ jobs: uses: actions/cache@v2 with: path: ${{ steps.determine-composer-cache-directory.outputs.directory }} - key: dependencies-os-${{ matrix.os }}-php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: dependencies-os-${{ matrix.os }}-php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-composer- + key: dependencies-os-${{ matrix.os }}-php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-${{ matrix.stability }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: dependencies-os-${{ matrix.os }}-php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-${{ matrix.stability }}-composer- - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -59,32 +62,10 @@ jobs: - name: Setup problem matchers for PHPUnit run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Install highest dependencies from composer.json - if: matrix.dependencies == 'highest' - uses: nick-invision/retry@v1 - with: - timeout_minutes: 5 - max_attempts: 5 - command: composer config platform --unset && composer update ${{ env.COMPOSER_FLAGS }} - - - name: Install lowest dependencies from composer.json - if: matrix.dependencies == 'lowest' - uses: nick-invision/retry@v1 - with: - timeout_minutes: 5 - max_attempts: 5 - command: composer install ${{ env.COMPOSER_FLAGS }} --prefer-lowest - - - name: Install dependencies from composer.lock - if: matrix.dependencies == 'locked' - uses: nick-invision/retry@v1 - with: - timeout_minutes: 5 - max_attempts: 5 - command: composer install ${{ env.COMPOSER_FLAGS }} - - - name: Install Laravel - run: composer require laravel/framework:${{ matrix.laravel-version }}.* --no-update + - name: Install dependencies + run: | + composer require laravel/framework:${{ matrix.laravel-version }}.* --no-interaction --no-update + composer update --${{ matrix.stability }} --prefer-dist --no-interaction - name: Run tests run: vendor/bin/phpunit diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index 65f30039..30062985 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -49,8 +49,8 @@ jobs: uses: actions/cache@v2 with: path: ${{ steps.determine-composer-cache-directory.outputs.directory }} - key: dependencies-os-${{ matrix.os }}-php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: dependencies-os-${{ matrix.os }}-php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-composer- + key: dependencies-os-${{ matrix.os }}-php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-prefer-stable-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: dependencies-os-${{ matrix.os }}-php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-prefer-stable-composer- - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2a540a9e..ae13dc8c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,7 +1,6 @@ name: Lint on: - push - - pull_request jobs: lint: name: Lint diff --git a/composer.json b/composer.json index 98a334bd..95833662 100644 --- a/composer.json +++ b/composer.json @@ -9,17 +9,17 @@ ], "license": "MIT", "require": { - "doctrine/dbal": "^3.3", - "illuminate/console": "^10.0|^11.0", - "illuminate/filesystem": "^10.0|^11.0", - "illuminate/support": "^10.0|^11.0", + "illuminate/console": "^10.38|^11.0", + "illuminate/database": "^10.38|^11.0", + "illuminate/filesystem": "^10.38|^11.0", + "illuminate/support": "^10.38|^11.0", "laravel-shift/faker-registry": "^0.3.0", "symfony/yaml": ">=6.2" }, "require-dev": { "laravel/pint": "^1.2", "mockery/mockery": "^1.4.4", - "orchestra/testbench": "^8.0", + "orchestra/testbench": "^8.0|^9.0", "phpunit/phpunit": "^10.0" }, "suggest": { diff --git a/src/EnumType.php b/src/EnumType.php deleted file mode 100644 index 2e897a8c..00000000 --- a/src/EnumType.php +++ /dev/null @@ -1,60 +0,0 @@ - "'" . $val . "'", - $this->values - ); - - return 'ENUM(' . implode(', ', $values) . ')'; - } - - public function convertToPHPValue($value, AbstractPlatform $platform) - { - return $value; - } - - public function convertToDatabaseValue($value, AbstractPlatform $platform) - { - if (!in_array($value, $this->values)) { - throw new \InvalidArgumentException("Invalid '" . $this->getName() . "' value."); - } - - return $value; - } - - public function getName(): string - { - return self::ENUM; - } - - public static function extractOptions($definition): array - { - $options = explode(',', preg_replace('/enum\((?P(.*))\)/', '$1', $definition)); - - return array_map( - function ($option) { - $raw_value = str_replace("''", "'", trim($option, "'")); - - if (!preg_match('/\s/', $raw_value)) { - return $raw_value; - } - - return sprintf('"%s"', $raw_value); - }, - $options - ); - } -} diff --git a/src/Tracer.php b/src/Tracer.php index 49207190..d954625a 100644 --- a/src/Tracer.php +++ b/src/Tracer.php @@ -2,9 +2,9 @@ namespace Blueprint; -use Doctrine\DBAL\Types\Type; use Illuminate\Database\Eloquent\Model; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Str; class Tracer { @@ -61,7 +61,7 @@ private function appClasses($paths): array return array_filter(array_map(function (\SplFIleInfo $file) { if ($file->getExtension() !== 'php') { - return; + return []; } $content = $this->filesystem->get($file->getPathName()); @@ -92,126 +92,113 @@ private function loadModel(string $class) private function extractColumns(Model $model): array { - $table = $model->getConnection()->getTablePrefix() . $model->getTable(); - $schema = $model->getConnection()->getDoctrineSchemaManager(); - - if (!Type::hasType('enum')) { - Type::addType('enum', EnumType::class); - $databasePlatform = $schema->getDatabasePlatform(); - $databasePlatform->registerDoctrineTypeMapping('enum', 'enum'); - } - - $database = null; - if (strpos($table, '.')) { - [$database, $table] = explode('.', $table); - } - - $columns = $schema->listTableColumns($table, $database); - - $uses_enums = collect($columns)->contains(fn ($column) => $column->getType() instanceof \Blueprint\EnumType); - - if ($uses_enums) { - $definitions = $model->getConnection()->getDoctrineConnection()->fetchAllAssociative($schema->getDatabasePlatform()->getListTableColumnsSQL($table, $database)); - - collect($columns)->filter(fn ($column) => $column->getType() instanceof \Blueprint\EnumType)->each(function ($column, $key) use ($definitions) { - $definition = collect($definitions)->where('Field', $key)->first(); - - $column->options = \Blueprint\EnumType::extractOptions($definition['Type']); - }); - } - - return $columns; + return $model->getConnection()->getSchemaBuilder()->getColumns($model->getTable()); } - /** - * @param \Doctrine\DBAL\Schema\Column[] $columns - */ private function mapColumns(array $columns): array { return collect($columns) - ->map([self::class, 'columns']) + ->keyBy('name') + ->map([self::class, 'columnAttributes']) ->toArray(); } - public static function columns(\Doctrine\DBAL\Schema\Column $column, string $key): string + public static function columnAttributes(array $column): string { $attributes = []; - $type = self::translations($column->getType()->getName()); + $type = self::translations($column); - if (in_array($type, ['decimal', 'float'])) { - if ($column->getPrecision()) { - $type .= ':' . $column->getPrecision(); + if (in_array($type, ['decimal', 'float', 'time', 'timetz', 'datetime', 'datetimetz', 'timestamp', 'timestamptz', 'geography', 'geometry']) + && str_contains($column['type'], '(')) { + $options = Str::between($column['type'], '(', ')'); + if ($options) { + $type .= ':' . $options; } - if ($column->getScale()) { - $type .= ',' . $column->getScale(); + } elseif (in_array($type, ['string', 'char']) && str_contains($column['type'], '(')) { + $length = Str::between($column['type'], '(', ')'); + if ($length != 255) { + $type .= ':' . $length; } - } elseif ($type === 'string' && $column->getLength()) { - if ($column->getLength() !== 255) { - $type .= ':' . $column->getLength(); - } - } elseif ($type === 'text') { - if ($column->getLength() > 65535) { - $type = 'longtext'; - } - } elseif ($type === 'enum' && !empty($column->options)) { - $type .= ':' . implode(',', $column->options); + } elseif (in_array($type, ['enum', 'set'])) { + $options = Str::between($column['type'], '(', ')'); + $type .= ':' . $options; } // TODO: guid/uuid $attributes[] = $type; - if ($column->getUnsigned()) { + if (str_contains($column['type'], 'unsigned')) { $attributes[] = 'unsigned'; } - if (!$column->getNotnull()) { + if ($column['nullable']) { $attributes[] = 'nullable'; } - if ($column->getAutoincrement()) { + if ($column['auto_increment']) { $attributes[] = 'autoincrement'; } - if (!is_null($column->getDefault())) { - $attributes[] = 'default:' . $column->getDefault(); + if ($column['default']) { + $attributes[] = 'default:' . $column['default']; } return implode(' ', $attributes); } - private static function translations(string $type): string + private static function translations(array $column): string { - static $mappings = [ - 'array' => 'string', - 'bigint' => 'biginteger', - 'binary' => 'binary', - 'blob' => 'binary', - 'boolean' => 'boolean', + $type = match ($column['type']) { + 'tinyint(1)', 'bit' => 'boolean', + 'nvarchar(max)' => 'text', + default => null, + }; + + $type ??= match ($column['type_name']) { + 'bigint', 'int8' => 'biginteger', + 'binary', 'varbinary', 'bytea', 'image', 'blob', 'tinyblob', 'mediumblob', 'longblob' => 'binary', + // 'bit', 'varbit' => 'bit', + 'boolean', 'bool' => 'boolean', + 'char', 'bpchar', 'nchar' => 'char', 'date' => 'date', - 'date_immutable' => 'date', - 'dateinterval' => 'date', - 'datetime' => 'datetime', - 'datetime_immutable' => 'datetime', - 'datetimetz' => 'datetimetz', - 'datetimetz_immutable' => 'datetimetz', - 'decimal' => 'decimal', + 'datetime', 'datetime2' => 'datetime', + 'datetimeoffset' => 'datetimetz', + 'decimal', 'numeric' => 'decimal', + 'double', 'float8' => 'double', 'enum' => 'enum', - 'float' => 'float', - 'guid' => 'string', - 'integer' => 'integer', + 'float', 'real', 'float4' => 'float', + 'geography' => 'geography', + 'geometry', 'geometrycollection', 'linestring', 'multilinestring', 'multipoint', 'multipolygon', 'point', 'polygon' => 'geometry', + // 'box', 'circle', 'line', 'lseg', 'path' => 'geometry', + 'integer', 'int', 'int4' => 'integer', + 'inet', 'cidr' => 'ipaddress', + // 'interval' => 'interval', 'json' => 'json', - 'object' => 'string', - 'simple_array' => 'string', - 'smallint' => 'smallinteger', - 'string' => 'string', - 'text' => 'text', + 'jsonb' => 'jsonb', + 'longtext' => 'longtext', + 'macaddr', 'macaddr8' => 'macadress', + 'mediumint' => 'mediuminteger', + 'mediumtext' => 'mediumtext', + // 'money', 'smallmoney' => 'money', + 'set' => 'set', + 'smallint', 'int2' => 'smallinteger', + 'text', 'ntext' => 'text', 'time' => 'time', - 'time_immutable' => 'time', - ]; - - return $mappings[$type] ?? 'string'; + 'timestamp' => 'timestamp', + 'timestamptz' => 'timestamptz', + 'timetz' => 'timetz', + 'tinyint' => 'tinyinteger', + 'tinytext' => 'tinytext', + 'uuid', 'uniqueidentifier' => 'uuid', + 'varchar', 'nvarchar' => 'string', + // 'xml' => 'xml', + 'year' => 'year', + default => null, + }; + + return $type ?? 'string'; } private function translateColumns(array $columns): array diff --git a/tests/Feature/Commands/TraceCommandTest.php b/tests/Feature/Commands/TraceCommandTest.php index 6e06f115..66439922 100644 --- a/tests/Feature/Commands/TraceCommandTest.php +++ b/tests/Feature/Commands/TraceCommandTest.php @@ -88,7 +88,7 @@ public function it_passes_the_command_path_to_tracer() } #[Test] - public function it_traces_models_with_differente_namespaces(): void + public function it_traces_models_with_different_namespaces(): void { $this->requireFixture('models/comment.php'); $this->requireFixture('models/custom-models-namespace.php'); diff --git a/tests/Unit/EnumTypeTest.php b/tests/Unit/EnumTypeTest.php deleted file mode 100644 index a428b16a..00000000 --- a/tests/Unit/EnumTypeTest.php +++ /dev/null @@ -1,29 +0,0 @@ -assertEquals($expected, \Blueprint\EnumType::extractOptions($definition)); - } - - public static function enumOptionsDataProvider(): array - { - return [ - ["enum('1','2','3')", [1, 2, 3]], - ["enum('One','Two','Three')", ['One', 'Two', 'Three']], - ["enum('Spaced and quoted names','John Doe','Connon O''Brien','O''Doul')", ['"Spaced and quoted names"', '"John Doe"', '"Connon O\'Brien"', 'O\'Doul']], - ]; - } -}