diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8d2e40f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,28 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +block_comment_start = /* +block_comment = * +block_comment_end = */ + +[*.md] +trim_trailing_whitespace = false +indent_size = unset +block_comment_start = unset +block_comment = unset +block_comment_end = unset + +[*.yml] +indent_size = 2 + +[phpunit.xml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..283a98b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/phpunit.xml.dist export-ignore +/phpunit.xml export-ignore +/tests export-ignore +/.editorconfig export-ignore +/composer.lock export-ignore +/.styleci.yml export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ee1b8f7 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# Let's keep it free and up to date +github: DarkGhostHunter +custom: "https://paypal.me/darkghosthunter" diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..aeab82b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,90 @@ +name: Bug Report +description: | + File a bug report to be fixed. + + Sponsors get priority issues, PRs, fixes and requests. Not a sponsor? [You're a just click away!](https://github.com/sponsors/DarkGhostHunter). +title: "[X.x] What does happen that is considered an error or bug?" +labels: ["bug"] +assignees: + - DarkGhostHunter +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + + The more detailed this bug report is, the faster it can be reviewed and fixed. + - type: input + id: version-php-os + attributes: + label: PHP & Platform + description: Exact PHP and Platform (OS) versions using this package. + placeholder: 8.1.2 - Ubuntu 22.04 x64 + validations: + required: true + - type: input + id: version-db + attributes: + label: Database + description: Exact DB version using this package, if applicable. + placeholder: MySQL 8.0.28 + validations: + required: false + - type: input + id: version-laravel + attributes: + label: Laravel version + description: Exact Laravel version using this package. + placeholder: 9.2.3 + validations: + required: true + - type: checkboxes + id: requirements + attributes: + label: Have you done this? + options: + - label: I have checked my logs and I'm sure is a bug in this package. + required: true + - label: I can reproduce this bug in isolation (vanilla Laravel install) + required: true + - label: I can suggest a workaround as a Pull Request + required: false + - type: textarea + id: expectation + attributes: + label: Expectation + description: Write what you expect to (correctly) happen. + placeholder: When I do this, I expect to happen that. + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: Write what (incorrectly) happens instead. + placeholder: Instead, when I do this, I receive that. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Paste the code to assert in a test, or just comment with the repository with the bug to download. + render: php + placeholder: | + $test = Laragear::make()->break(); + + static::assertFalse($test); + + // or comment with "https://github.com/my-name/my-bug-report" + validations: + required: true + - type: textarea + id: logs + attributes: + label: Stack trace & logs + description: If you have a **full** stack trace, you can copy it here. You may hide sensible information. + placeholder: This is automatically formatted into code, no need for ``` backticks. + render: shell + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..cc1acf3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,51 @@ +name: Feature request +description: | + Suggest a feature for this package. + + Sponsors get priority issues, PRs, fixes and requests. Not a sponsor? [You're a just click away!](https://github.com/sponsors/DarkGhostHunter). +title: "[X.x] Add this cool feature for this package" +labels: ["enhancement"] +assignees: + - DarkGhostHunter +body: + - type: markdown + attributes: + value: | + Thanks for contributing to this package! + New features keep this package fresh and fun for everybody to use. + - type: checkboxes + id: requirements + attributes: + label: Please check these requirements + options: + - label: This feature helps everyone using this package + required: true + - label: It's feasible and maintainable + required: true + - label: It's non breaking + required: true + - label: I issued a PR with the implementation (optional) + required: false + - type: textarea + id: description + attributes: + label: Description + description: Describe how the feature works + placeholder: | + This new feature would accomplish... + + It could be implemented by doing... + + And it would be cool because... + validations: + required: true + - type: textarea + id: sample + attributes: + label: Code sample + description: Sample a small snippet on how the feature works + placeholder: | + Laragear::newFeature()->cool(); + render: php + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..200a6d1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ + + +# Description + +This feature/fix allows to... + +# Code samples + +```php +Laragear::sample(); +``` diff --git a/.github/assets/support.png b/.github/assets/support.png new file mode 100644 index 0000000..8276da2 Binary files /dev/null and b/.github/assets/support.png differ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7995edd --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: composer + directory: "/" + schedule: + interval: daily + time: "09:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..7d9e607 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,131 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow + +name: Tests + +on: + push: + pull_request: + +jobs: + + byte_level: + name: 0️⃣ Byte-level + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Check file permissions + run: | + test $(find . -type f -not -path './.git/*' -executable) == + - name: Find non-printable ASCII characters + run: | + ! LC_ALL=C.UTF-8 find ./src -type f -name *.php -print0 | xargs -0 -- grep -PHn [^ -~] + + syntax_errors: + name: 1️⃣ Syntax errors + runs-on: ubuntu-latest + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: latest + tools: parallel-lint + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Validate Composer configuration + run: composer validate --strict + + - name: Check source code for syntax errors + run: composer exec -- parallel-lint src/ + + unit_tests: + name: 2️⃣ Unit and Feature tests + needs: + - byte_level + - syntax_errors + runs-on: ubuntu-latest + strategy: + matrix: + php-version: + - 8.1 + - 8.2 + - 8.3 + laravel-constraint: + - 10.* + - 11.* + dependencies: + - lowest + - highest + exclude: + - php-version: 8.1 + laravel-constraint: 11.* + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, intl + coverage: xdebug + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + uses: ramsey/composer-install@v3 + with: + dependency-versions: ${{ matrix.dependencies }} + composer-options: --with=laravel/framework:${{ matrix.laravel-constraint }} + + - name: Execute unit tests + run: composer run-script test + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + + static_analysis: + name: 3️⃣ Static Analysis + needs: + - byte_level + - syntax_errors + runs-on: ubuntu-latest + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + tools: phpstan + php-version: latest + coverage: none + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Validate Composer configuration + run: composer validate --strict + + - name: Install dependencies + uses: ramsey/composer-install@v3 + + - name: Execute static analysis + run: composer exec -- phpstan analyze -l 5 src/ + + exported_files: + name: 4️⃣ Exported files + needs: + - byte_level + - syntax_errors + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check exported files + run: | + EXPECTED=LICENSE.md,README.md,composer.json + CURRENT=$(git archive HEAD | tar --list --exclude=src --exclude=src/* --exclude=.stubs --exclude=.stubs/* --exclude=lang --exclude=lang/* --exclude=config --exclude=config/* --exclude=database --exclude=database/* --exclude=resources --exclude=resources/* | paste -s -d ,) + echo CURRENT =${CURRENT} + echo EXPECTED=${EXPECTED} + test ${CURRENT} == ${EXPECTED} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5da7a88 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/build +/vendor +/.idea +.php-cs-fixer.cache +.phpunit.result.cache +composer.lock +phpunit.xml.bak diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..606f430 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,2 @@ +preset: laravel +version: 8.1 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..2cd3913 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Italo Israel Baeza Cabrera + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ceeb2ba --- /dev/null +++ b/README.md @@ -0,0 +1,254 @@ +# Meta Model +[![Latest Version on Packagist](https://img.shields.io/packagist/v/laragear/meta-model.svg)](https://packagist.org/packages/laragear/meta-model) +[![Latest stable test run](https://github.com/Laragear/MetaModel/workflows/Tests/badge.svg)](https://github.com/Laragear/MetaModel/actions) +[![Codecov coverage](https://codecov.io/gh/Laragear/MetaModel/branch/1.x/graph/badge.svg?token=5COE8X0JMJ)](https://codecov.io/gh/Laragear/MetaModel) +[![Maintainability](https://api.codeclimate.com/v1/badges/677b55bbf19bda17e0f5/maintainability)](https://codeclimate.com/github/Laragear/MetaModel/maintainability) +[![Sonarcloud Status](https://sonarcloud.io/api/project_badges/measure?project=Laragear_MetaModel&metric=alert_status)](https://sonarcloud.io/dashboard?id=Laragear_MetaModel) +[![Laravel Octane Compatibility](https://img.shields.io/badge/Laravel%20Octane-Compatible-success?style=flat&logo=laravel)](https://laravel.com/docs/11.x/octane#introduction) + +Let developers customize your model and migrations. + +```php +use Illuminate\Database\Eloquent\Model; +use Laragear\MetaModel\CustomizableModel; + +class MyPackageModel extends Model +{ + use CustomizableModel; +} +``` + +## Keep this package free + +[![](.github/assets/support.png)](https://github.com/sponsors/DarkGhostHunter) + +Your support allows me to keep this package free, up-to-date and maintainable. Alternatively, you can **[spread the word!](http://twitter.com/share?text=I%20am%20using%20this%20cool%20PHP%20package&url=https://github.com%2FLaragear%2FRut&hashtags=PHP,Laravel,Chile)** + +## Requirements + +- Laravel 10 or later + +## Installation + +Fire up Composer and require it into your package: + +```bash +composer require --dev laragear/meta-model +``` + +## Customizing models + +Most of the time, your users will want to customize the models and migrations in your package. For example, they would want to add columns and cast them to specific data types, or modify which properties are hidden. This can be done with a model that incorporates the `CustomizableModel` trait. + +```php +namespace MyVendor\MyPackage\Models; + +use Illuminate\Database\Eloquent\Model; +use Laragear\MetaModel\CustomizableModel; + +class MyModel extends Model +{ + use CustomizableModel; + + // ... +} +``` + +From there, the end-developer can customize the model using static properties: + +- `$useTable`: A custom table name to use. +- `$useCasts`: The casts attributes to merge. +- `$useFillable`: The fillable attributes to merge. +- `$useGuarded`: The guarded attributes to merge. +- `$useHidden`: The hidden attributes to merge. +- `$useVisible`: The visible attributes to merge. +- `$useAppends`: The appends attributes to merge. + +All of these static properties, except for `$useTable`, accept a Closure that receives the model and returns the attributes. The end-developer should modify these properties in the `boot()` method of the `AppServiceProvider`. + +```php +namespace App\Providers; + +use MyVendor\MyPackage\Models\Car; +use Illuminate\Support\ServiceProvider; + +class AppServiceProvider extends ServiceProvider +{ + /** + * Bootstrap any application services. + */ + public function boot(): void + { + Car::$useCasts = ['is_new' => 'boolean']; + } +} +``` + +## Customizable Migration + +To allow customizable migrations, you can create a standard migration, like `0000_00_00_0000_create_models_table.php`, that should return a new migration class. Instead of returning a class that extends the default `Migration` class, let it extend your own Migration class that extends `CustomizableMigration`, and add the name of the Model to the `$model` class. + +For example, let's say we want to create a migration for a Car model. We create our own class to migrate, extending the `CustomizableMigration` class. + +```php +namespace MyVendor\MyPackage\Migrations; + +use Illuminate\Database\Schema\Blueprint; +use Laragear\MetaModel\CustomizableMigration; +use MyVendor\MyPackage\Models\Car; + +/** + * @internal + */ +abstract class CarsMigration extends CustomizableMigration +{ + protected static $model = Car::class + + protected function create(Blueprint $table) + { + $table->id(); + + $table->string('manufacturer'); + $table->string('model'); + $table->tinyInteger('year'); + + $table->timestamps(); + } +} +``` + +After defining our default migration class, we use create the migration file `0000_00_00_0000_create_cars_table.php`. Instead of returning a class that extends the default Laravel migration, we use our `CarsMigration` class. + +```php +use MyVendor\MyPackage\Migrations\CarsMigration; +use Illuminate\Database\Schema\Blueprint; + +return new class extends CarsMigration +{ + // .. +} +``` + +### Adding Custom Columns + +You may want to let the end-developer to add additional columns to the migration. For that, just call `addCustomColumns()` anywhere inside the `create()` method, ensuring you pass the `Blueprint` instance. A great place to call this is just before the `timestamps()` call. + +```php +namespace MyVendor\MyPackage\Migrations; + +use Illuminate\Database\Schema\Blueprint; +use Laragear\MetaModel\CustomizableMigration; + +/** + * @internal + */ +abstract class CarsMigration extends CustomizableMigration +{ + protected function create(Blueprint $table) + { + $table->id(); + + $table->string('manufacturer'); + $table->string('model'); + $table->tinyInteger('year'); + + $this->addAdditionalColumns($table); + + $table->timestamps(); + } +} +``` + +After that, in your migration file, add the `addCustomColumns()` method so the end-developer knows he can extend the table schema. + +```php +use MyVendor\MyPackage\Migrations\CarsMigration; +use Illuminate\Database\Schema\Blueprint; + +return new class extends CarsMigration +{ + /** + * Add additional columns to the table. + */ + public function addCustomColumns(Blueprint $table): void + { + // ... + } +} +``` + +### Morphs + +If your migration requires a morph relationship, you will find that end-developers will have models with different model types. To avoid setting a morph to a type, use the `createMorphRelation()` method with the `Blueprint` instance and the name of the morph type. + +```php +protected function create(Blueprint $table) +{ + $table->id(); + + $this->createMorphRelation($table, 'ownable'); + + $table->string('manufacturer'); + $table->string('model'); + $table->tinyInteger('year'); + + $this->addAdditionalColumns($table); + + $table->timestamps(); +} +``` + +This will let the end-developer to change the morph type through the `$morphsType` and `$morphIndex` properties. For example, if he's using ULID morphs, he may set it easily: + +```php +use MyVendor\MyPackage\Migrations\CarsMigration; +use Illuminate\Database\Schema\Blueprint; + +return new class extends CarsMigration +{ + protected string $morphsType = 'ulid'; +} +``` + +### After Up & Before Down + +The `CustomizableMigration` contains two methods, `afterUp()` and `beforeDown()`. The first is executed after the table is created, while the latter is executed before the table is dropped. This allows the developer to run custom logic to enhance its migrations, or avoid failing migrations. + +For example, the end-developer can use these methods to create foreign column references, and remove them before dropping the table. + +```php +use MyVendor\MyPackage\Migrations\CarsMigration; +use Illuminate\Database\Schema\Blueprint; + +return new class extends CarsMigration +{ + public function afterUp(Blueprint $table): void + { + $table->foreign('manufacturer')->references('name')->on('manufacturers'); + } + + public function beforeDown(Blueprint $table): void + { + $table->dropForeign('manufacturer'); + } +} +``` + +## Laravel Octane compatibility + +- There are no singletons using a stale application instance. +- There are no singletons using a stale config instance. +- There are no singletons using a stale request instance. +- Trait static properties are only written once by end-developer. + +There should be no problems using this package with Laravel Octane. + +## Security + +If you discover any security related issues, please email darkghosthunter@gmail.com instead of using the issue tracker. + +# License + +This specific package version is licensed under the terms of the [MIT License](LICENSE.md), at time of publishing. + +[Laravel](https://laravel.com) is a Trademark of [Taylor Otwell](https://github.com/TaylorOtwell/). Copyright © 2011-2024 Laravel LLC. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..203bc0f --- /dev/null +++ b/composer.json @@ -0,0 +1,61 @@ +{ + "name": "laragear/meta-model", + "description": "Configurable Model and Migration for packages", + "type": "library", + "license": "MIT", + "minimum-stability": "dev", + "prefer-stable": true, + "keywords": [ + "laravel", + "model", + "database", + "eloquent" + ], + "authors": [ + { + "name": "Italo Israel Baeza Cabrera", + "email": "DarkGhostHunter@Gmail.com", + "role": "Developer", + "homepage": "https://github.com/sponsors/DarkGhostHunter" + } + ], + "support": { + "source": "https://github.com/Laragear/Rewind", + "issues": "https://github.com/Laragear/Rewind/issues" + }, + "require": { + "php": "^8.1", + "illuminate/database": "10.*|11.*" + }, + "require-dev": { + "phpunit/phpunit": "10.*|11.*", + "mockery/mockery": "^1.6" + }, + "autoload": { + "psr-4": { + "Laragear\\MetaModel\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests" + } + }, + "scripts": { + "test": "vendor/bin/phpunit --coverage-clover build/logs/clover.xml", + "test-coverage": "vendor/bin/phpunit --coverage-html coverage" + }, + "config": { + "sort-packages": true + }, + "funding": [ + { + "type": "Github Sponsorship", + "url": "https://github.com/sponsors/DarkGhostHunter" + }, + { + "type": "Paypal", + "url": "https://paypal.me/darkghosthunter" + } + ] +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..09e4358 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,30 @@ + + + + + + + + + + tests + + + + + + + + + + + + + + src + + + src/LararutServiceProvider.php + + + diff --git a/src/CustomizableMigration.php b/src/CustomizableMigration.php new file mode 100644 index 0000000..bb76dc7 --- /dev/null +++ b/src/CustomizableMigration.php @@ -0,0 +1,111 @@ +getTable(), $this->create(...)); + + Schema::table($this->getTable(), $this->afterUp(...)); + } + + /** + * Create a new morph relation. + */ + protected function createMorphRelation(Blueprint $table, string $name): void + { + match (strtolower($this->morphsType)) { + 'integer', 'int', 'numeric' => $table->numericMorphs($name, $this->morphIndex), + 'uuid' => $table->uuidMorphs($name, $this->morphIndex), + 'ulid' => $table->ulidMorphs($name, $this->morphIndex), + default => $table->morphs($name, $this->morphIndex) + }; + } + + /** + * Create the table columns. + */ + abstract public function create(Blueprint $table): void; + + /** + * Add additional columns to the table. + */ + public function addCustomColumns(Blueprint $table): void + { + // ... + } + + /** + * Execute logic after the table is created. + */ + protected function afterUp(Blueprint $table): void + { + // ... + } + + /** + * Execute logic before the table is dropped. + */ + protected function beforeDown(Blueprint $table): void + { + // ... + } + + /** + * Reverse the migrations. + * + * @internal + * @return void + */ + public function down(): void + { + Schema::table($this->getTable(), $this->beforeDown(...)); + + Schema::dropIfExists($this->getTable()); + } + + /** + * Return the table name of the model to use in the migration. + * + * @internal + */ + private function getTable(): string + { + return static::$model + ? (new static::$model)->getTable() + : throw new UnexpectedValueException('The ' . static::class . '::$model must have a Model class name.'); + } +} diff --git a/src/CustomizableModel.php b/src/CustomizableModel.php new file mode 100644 index 0000000..cfd19f8 --- /dev/null +++ b/src/CustomizableModel.php @@ -0,0 +1,100 @@ +)|array + */ + public static $useCasts = []; + + /** + * The fillable attributes to merge with the default model. + * + * @var (\Closure():string[])|string[] + */ + public static $useFillable = []; + + /** + * The fillable attributes to merge with the default model. + * + * @var (\Closure():string[])|string[] + */ + public static $useGuarded = []; + + /** + * The hidden attributes to merge with the default model. + * + * @var (\Closure():string[])|string[] + */ + public static $useHidden = []; + + /** + * The visible attributes to merge with the default model. + * + * @var (\Closure():string[])|string[] + */ + public static $useVisible = []; + + /** + * The fillable attributes to merge with the default model. + * + * @var (\Closure():string[])|string[] + */ + public static $useAppends = []; + + /** + * Boot the trait. + */ + protected static function bootCustomizableModel(): void + { + static::$useTable ??= Str::snake(Str::pluralStudly(class_basename(static::class))); + } + + /** + * Initialize the trait. + */ + protected function initializeCustomizableModel(): void + { + $this->table = static::$useTable; + + $resolve = static function (Closure|array $value, $model): array { + return is_array($value) ? $value : ($value)($model); + }; + + $this->mergeCasts($resolve(static::$useCasts, $this)); + $this->mergeFillable($resolve(static::$useFillable, $this)); + $this->mergeGuarded($resolve(static::$useGuarded, $this)); + + if (static::$useHidden) { + $this->setHidden(array_merge($this->hidden, $resolve(static::$useHidden, $this))); + } + + if (static::$useVisible) { + $this->setVisible(array_merge($this->visible, $resolve(static::$useVisible, $this))); + } + + if (static::$useAppends) { + $this->setAppends(array_merge($this->appends, $resolve(static::$useAppends, $this))); + } + } +} diff --git a/tests/CustomizableMigrationTest.php b/tests/CustomizableMigrationTest.php new file mode 100644 index 0000000..eef1284 --- /dev/null +++ b/tests/CustomizableMigrationTest.php @@ -0,0 +1,234 @@ +container = Container::setInstance(); + + TestMigrationWithMorph::$type = ''; + } + + #[Test] + public function creates_columns(): void + { + $blueprint = m::mock(Blueprint::class); + $blueprint->expects('createCall')->once(); + $blueprint->expects('customColumns')->once(); + $blueprint->expects('afterUpCall')->once(); + + Schema::expects('create')->withArgs(function (string $table, Closure $closure) use ($blueprint): bool { + static::assertSame('test_migration_models', $table); + $closure($blueprint); + + return true; + }); + + Schema::expects('table')->withArgs(function (string $table, Closure $closure) use ($blueprint): bool { + static::assertSame('test_migration_models', $table); + $closure($blueprint); + + return true; + }); + + $migration = new TestMigration(); + + $migration->up(); + } + + #[Test] + public function creates_default_morph(): void + { + TestMigrationWithMorph::$type = ''; + + $blueprint = m::mock(Blueprint::class); + $blueprint->expects('createCall'); + $blueprint->expects('morphs')->with('foo', null); + + Schema::expects('create')->withArgs(function (string $table, Closure $closure) use ($blueprint): bool { + static::assertSame('test_migration_models', $table); + + $closure($blueprint); + + return true; + }); + + Schema::expects('table'); + + $migration = new TestMigrationWithMorph(); + + $migration->up(); + } + + #[Test] + public function creates_numeric_morph(): void + { + TestMigrationWithMorph::$type = 'numeric'; + + $blueprint = m::mock(Blueprint::class); + $blueprint->expects('createCall'); + $blueprint->expects('numericMorphs')->with('foo', null); + + Schema::expects('create')->withArgs(function (string $table, Closure $closure) use ($blueprint): bool { + static::assertSame('test_migration_models', $table); + $closure($blueprint); + + return true; + }); + + Schema::expects('table'); + + $migration = new TestMigrationWithMorph(); + + $migration->up(); + } + + #[Test] + public function creates_uuid_morph(): void + { + TestMigrationWithMorph::$type = 'uuid'; + + $blueprint = m::mock(Blueprint::class); + $blueprint->expects('createCall'); + $blueprint->expects('uuidMorphs')->with('foo', null); + + Schema::expects('create')->withArgs(function (string $table, Closure $closure) use ($blueprint): bool { + static::assertSame('test_migration_models', $table); + $closure($blueprint); + + return true; + }); + + Schema::expects('table'); + + $migration = new TestMigrationWithMorph(); + + $migration->up(); + } + + #[Test] + public function creates_ulid_morph(): void + { + TestMigrationWithMorph::$type = 'ulid'; + + $blueprint = m::mock(Blueprint::class); + $blueprint->expects('createCall'); + $blueprint->expects('ulidMorphs')->with('foo', null); + + Schema::expects('create')->withArgs(function (string $table, Closure $closure) use ($blueprint): bool { + static::assertSame('test_migration_models', $table); + $closure($blueprint); + + return true; + }); + + Schema::expects('table'); + + $migration = new TestMigrationWithMorph(); + + $migration->up(); + } + + #[Test] + public function drops_table(): void + { + $blueprint = m::mock(Blueprint::class); + $blueprint->expects('beforeDownCall'); + + Schema::expects('table')->withArgs(function (string $table, Closure $closure) use ($blueprint): bool { + static::assertSame('test_migration_models', $table); + $closure($blueprint); + + return true; + }); + + Schema::expects('dropIfExists')->with('test_migration_models'); + + $migration = new TestMigrationWithMorph(); + + $migration->down(); + } + + #[Test] + public function throws_if_model_not_set(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('The Tests\TestMigrationWithoutModel::$model must have a Model class name'); + + (new TestMigrationWithoutModel)->up(); + } +} + +class TestMigration extends CustomizableMigration +{ + protected static $model = TestMigrationModel::class; + + public function create(Blueprint $table): void + { + $table->createCall(); + } + + public function addCustomColumns(Blueprint $table): void + { + $table->customColumns(); + } + + protected function afterUp(Blueprint $table): void + { + $table->afterUpCall(); + } + + protected function beforeDown(Blueprint $table): void + { + $table->beforeDownCall(); + } +} + +class TestMigrationWithMorph extends CustomizableMigration +{ + protected static $model = TestMigrationModel::class; + + public static $type; + + public function create(Blueprint $table): void + { + $table->createCall(); + } + + public function addCustomColumns(Blueprint $table): void + { + $this->morphsType = static::$type; + + $this->createMorphRelation($table, 'foo'); + } +} + +class TestMigrationWithoutModel extends CustomizableMigration +{ + public function create(Blueprint $table): void + { + // + } +} + +class TestMigrationModel extends Model +{ + +} diff --git a/tests/CustomizableModelTest.php b/tests/CustomizableModelTest.php new file mode 100644 index 0000000..91618e1 --- /dev/null +++ b/tests/CustomizableModelTest.php @@ -0,0 +1,140 @@ +getTable()); + } + + #[Test] + public function uses_custom_table_name(): void + { + static::assertSame('test_models', (new TestModel())->getTable()); + } + + #[Test] + public function merges_casts(): void + { + TestModel::$useCasts = ['foo' => 'bar']; + + static::assertSame(['id' => 'int', 'foo' => 'bar'], (new TestModel())->getCasts()); + } + + #[Test] + public function merges_casts_callback(): void + { + TestModel::$useCasts = fn(TestModel $model) => ['foo' => 'bar']; + + static::assertSame(['id' => 'int', 'foo' => 'bar'], (new TestModel())->getCasts()); + } + + #[Test] + public function merges_fillable(): void + { + TestModel::$useFillable = ['foo', 'bar']; + + static::assertSame(['foo', 'bar'], (new TestModel())->getFillable()); + } + + #[Test] + public function merges_fillable_callback(): void + { + TestModel::$useFillable = fn(TestModel $model) => ['foo', 'bar']; + + static::assertSame(['foo', 'bar'], (new TestModel())->getFillable()); + } + + #[Test] + public function merges_guarded(): void + { + TestModel::$useGuarded = ['foo', 'bar']; + + static::assertSame(['*', 'foo', 'bar'], (new TestModel())->getGuarded()); + } + + #[Test] + public function merges_guarded_callback(): void + { + TestModel::$useGuarded = fn(TestModel $model) => ['foo', 'bar']; + + static::assertSame(['*', 'foo', 'bar'], (new TestModel())->getGuarded()); + } + + #[Test] + public function merges_hidden(): void + { + TestModel::$useHidden = ['foo', 'bar']; + + static::assertSame(['baz', 'foo', 'bar'], (new TestModel())->getHidden()); + } + + #[Test] + public function merges_hidden_callback(): void + { + TestModel::$useHidden = fn(TestModel $model) => ['foo', 'bar']; + + static::assertSame(['baz', 'foo', 'bar'], (new TestModel())->getHidden()); + } + + #[Test] + public function merge_visible(): void + { + TestModel::$useVisible = ['foo', 'bar']; + + static::assertSame(['quz', 'foo', 'bar'], (new TestModel())->getVisible()); + } + + #[Test] + public function merge_visible_callback(): void + { + TestModel::$useVisible = fn(TestModel $model) => ['foo', 'bar']; + + static::assertSame(['quz', 'foo', 'bar'], (new TestModel())->getVisible()); + } + + #[Test] + public function merge_appends(): void + { + TestModel::$useAppends = ['foo', 'bar']; + + static::assertSame(['qux', 'foo', 'bar'], (new TestModel())->getAppends()); + } + + #[Test] + public function merge_appends_callback(): void + { + TestModel::$useAppends = fn(TestModel $model) => ['foo', 'bar']; + + static::assertSame(['qux', 'foo', 'bar'], (new TestModel())->getAppends()); + } +} + +class TestModel extends Model +{ + use CustomizableModel; + + protected $hidden = ['baz']; + protected $visible = ['quz']; + protected $appends = ['qux']; +}