From 997137b831e5ec07051e7347b3741d99dc52d120 Mon Sep 17 00:00:00 2001 From: Watheq Alshowaiter Date: Fri, 7 Nov 2025 11:55:23 +0300 Subject: [PATCH 01/11] WIP uncle --- src/Support/Helpers.php | 103 ++++++++++++++++++ tests/FieldsTest.php | 11 ++ tests/Models/Uncle.php | 29 +++++ .../2025_11_07_100247_create_uncles_table.php | 23 ++++ todos.md | 2 +- 5 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 tests/Models/Uncle.php create mode 100644 tests/database/migrations/2025_11_07_100247_create_uncles_table.php diff --git a/src/Support/Helpers.php b/src/Support/Helpers.php index c64ddd3..3f73dc7 100644 --- a/src/Support/Helpers.php +++ b/src/Support/Helpers.php @@ -2,6 +2,7 @@ namespace WatheqAlshowaiter\ModelFields\Support; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\App; /** @@ -31,4 +32,106 @@ public static function getTableFromThisModel($model) return str_replace('.', '__', $table); } + + /** + * Get fields that are automatically filled by model observers/events + * during 'creating' and 'saving' events + * + * @param class-string $model + * @return string[] + */ + public static function getObserverFilledFields($model) + { + /** @var Model $modelInstance */ + $modelInstance = new $model; + + // Get attributes before firing events + $attributesBeforeEvents = array_keys($modelInstance->getAttributes()); + + // Fire the creating and saving events to trigger observers + $modelInstance->fireModelEvent('creating', false); + $modelInstance->fireModelEvent('saving', false); + + // Get attributes after firing events + $attributesAfterEvents = array_keys($modelInstance->getAttributes()); + + // Get all database fields + $allFields = self::getAllFieldsForModel($model); + + // Return only the new fields that were added by observers/events + // and are actual database fields + return collect($attributesAfterEvents) + ->diff($attributesBeforeEvents) + ->filter(function ($field) use ($allFields) { + return in_array($field, $allFields); + }) + ->values() + ->toArray(); + } + + /** + * Helper to get all fields for a model (used internally) + * + * @param class-string $model + * @return string[] + */ + private static function getAllFieldsForModel($model) + { + if (self::isLaravelVersionLessThan10()) { + // For older versions, we need to query the database + $table = self::getTableFromThisModel($model); + $databaseDriver = \Illuminate\Support\Facades\DB::connection()->getDriverName(); + + switch ($databaseDriver) { + case 'sqlite': + $queryResult = \Illuminate\Support\Facades\DB::select("PRAGMA table_info($table)"); + + return collect($queryResult) + ->map(fn ($column) => (array) $column) + ->pluck('name') + ->toArray(); + case 'mysql': + case 'mariadb': + $queryResult = \Illuminate\Support\Facades\DB::select( + 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION ASC', + [$table] + ); + + return collect($queryResult) + ->map(fn ($column) => (array) $column) + ->pluck('name') + ->toArray(); + case 'pgsql': + $queryResult = \Illuminate\Support\Facades\DB::select( + 'SELECT column_name AS name FROM information_schema.columns WHERE table_name = ? ORDER BY ordinal_position ASC', + [$table] + ); + + return collect($queryResult) + ->map(fn ($column) => (array) $column) + ->pluck('name') + ->toArray(); + case 'sqlsrv': + $queryResult = \Illuminate\Support\Facades\DB::select( + 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = SCHEMA_NAME() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION ASC', + [$table] + ); + + return collect($queryResult) + ->map(fn ($column) => (array) $column) + ->pluck('name') + ->toArray(); + default: + return []; + } + } + + $table = self::getTableFromThisModel($model); + + return collect(\Illuminate\Support\Facades\Schema::getColumns($table)) + ->pluck('name') + ->unique() + ->values() + ->toArray(); + } } diff --git a/tests/FieldsTest.php b/tests/FieldsTest.php index 457bce2..6d00ac7 100644 --- a/tests/FieldsTest.php +++ b/tests/FieldsTest.php @@ -17,6 +17,7 @@ use WatheqAlshowaiter\ModelFields\Tests\Models\Mother; use WatheqAlshowaiter\ModelFields\Tests\Models\Someone; use WatheqAlshowaiter\ModelFields\Tests\Models\Son; +use WatheqAlshowaiter\ModelFields\Tests\Models\Uncle; class FieldsTest extends TestCase { @@ -374,6 +375,16 @@ public function test_default_fields_for_brother_model() $this->assertEquals($expected, Brother::defaultFields()); } + public function test_required_fields_for_uncles_model() + { + $expected = [ + 'column_2', + ]; + + $this->assertEquals($expected, Fields::model(Uncle::class)->requiredFields()); + //$this->assertEquals($expected, Uncle::defaultFields()); + } + /** * @throws ReflectionException */ diff --git a/tests/Models/Uncle.php b/tests/Models/Uncle.php new file mode 100644 index 0000000..a776a12 --- /dev/null +++ b/tests/Models/Uncle.php @@ -0,0 +1,29 @@ + UserSaved::class, + // 'deleted' => UserDeleted::class, + //]; + protected static function boot(): void + { + parent::boot(); + + self::creating(function ($model) { + $model->column_1 = 'creating'; + }); + + self::saving(function ($model) { + // todo if model is new then it will be created + $model->column_1 = 'created'; + }); + } +} + +// todo UncleObserver diff --git a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php new file mode 100644 index 0000000..ee9fbf9 --- /dev/null +++ b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php @@ -0,0 +1,23 @@ +string('column_1'); // add by creating boot method in model + $table->string('column_2'); //added by observer creating boot method in model + }); + } + + public function down(): void + { + Schema::dropIfExists('uncles'); + } +} diff --git a/todos.md b/todos.md index cff7cd7..fc96895 100644 --- a/todos.md +++ b/todos.md @@ -67,5 +67,5 @@ - [x] test opening the url (mocking with reflectionClass) - [x] make sure $this->input->isInteractive() it enough - [ ] add new changes in using the terminal to the docs -- [ ] (next version) exclude from required fields that are filled in "creating" observers/events and add test cases for +- [ ] (next version) exclude from required fields that are filled in "creating/saving" observers/events and add test cases for that From 4b3e9bfdf815179bec15ba8e0a80d86770141098 Mon Sep 17 00:00:00 2001 From: Watheq Alshowaiter Date: Fri, 7 Nov 2025 22:19:05 +0300 Subject: [PATCH 02/11] feat: exclude observer/event-filled fields from required fields - Added observer field detection to FieldsService and ModelFieldsServiceProvider - Simplified getObserverFilledFields() to use event dispatching for better reliability - Created UncleObserver class demonstrating observer pattern event handling - Added UncleCreating and UncleSaving event classes with proper event listeners - Registered event listeners in TestCase for both dispatched and eloquent raw events - Updated Uncle model migration with 6 columns to test all event patterns - Added comprehensive test cases for auto-filled field detection --- src/FieldsService.php | 41 ++++--- src/ModelFieldsServiceProvider.php | 4 + src/Support/Helpers.php | 106 +++--------------- tests/FieldsTest.php | 21 ++-- tests/Listeners/UncleCreatingListener.php | 13 +++ tests/Listeners/UncleSavingListener.php | 13 +++ tests/Models/Uncle.php | 50 +++++++-- tests/TestCase.php | 23 ++++ .../2025_11_07_100247_create_uncles_table.php | 8 +- todos.md | 2 +- 10 files changed, 150 insertions(+), 131 deletions(-) create mode 100644 tests/Listeners/UncleCreatingListener.php create mode 100644 tests/Listeners/UncleSavingListener.php diff --git a/src/FieldsService.php b/src/FieldsService.php index 7631ea3..f847fb3 100644 --- a/src/FieldsService.php +++ b/src/FieldsService.php @@ -20,11 +20,12 @@ class FieldsService * Set up the model class to get fields from * * @param class-string $modelClass + * * @return $this */ public function model($modelClass) { - if (! $this->isEloquentModelClass($modelClass)) { + if (!$this->isEloquentModelClass($modelClass)) { throw new InvalidModelClassException('Model class must be an instance of Eloquent model'); } @@ -83,6 +84,7 @@ public function requiredFields() } $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->modelClass); + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->modelClass); $primaryIndex = $this->primaryField(); @@ -105,6 +107,9 @@ public function requiredFields() ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); }) + ->reject(function ($column) use ($observerDefaultAttributes) { + return in_array($column['name'], $observerDefaultAttributes); + }) ->pluck('name') ->unique() ->values() @@ -192,7 +197,7 @@ public function databaseDefaultFields() return $column; }) ->filter(function ($column) use ($primaryField) { - return $column['default'] !== null && ! (in_array($column['name'], $primaryField)); + return $column['default'] !== null && !(in_array($column['name'], $primaryField)); }) ->pluck('name') ->unique() @@ -384,7 +389,7 @@ protected function requiredFieldsForSqlite() ->reject(function ($column) { return $column['pk'] || $column['dflt_value'] != null - || ! $column['notnull']; + || !$column['notnull']; }) ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); @@ -463,7 +468,7 @@ protected function nullableFieldsForSqlite() return (array) $column; }) ->filter(function ($column) { - return ! $column['notnull']; + return !$column['notnull']; }) ->pluck('name') ->toArray(); @@ -483,7 +488,7 @@ protected function requiredFieldsForMysqlAndMariaDb() $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->modelClass); $queryResult = DB::select( - /** @lang SQLite */ " + /** @lang SQLite */ " SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type, @@ -531,7 +536,7 @@ protected function databaseDefaultFieldsForMysqlAndMariaDb() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang SQLite */ " + /** @lang SQLite */ " SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type, @@ -574,7 +579,7 @@ protected function primaryFieldForMysqlAndMariaDb() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang SQLite */ " + /** @lang SQLite */ " SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type, @@ -617,7 +622,7 @@ protected function allFieldsForMysqlAndMariaDb() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang SQLite */ ' + /** @lang SQLite */ ' SELECT COLUMN_NAME AS name FROM @@ -646,7 +651,7 @@ protected function nullableFieldsForMysqlAndMariaDb() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang SQLite */ " + /** @lang SQLite */ " SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type, @@ -740,7 +745,7 @@ protected function allFieldsForPostgres() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang PostgreSQL */ ' + /** @lang PostgreSQL */ ' SELECT is_nullable AS nullable, column_name AS name, @@ -771,7 +776,7 @@ protected function nullableFieldsForPostgres() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang PostgreSQL */ ' + /** @lang PostgreSQL */ ' SELECT is_nullable AS nullable, column_name AS name @@ -842,7 +847,7 @@ protected function requiredFieldsForPostgres() ->toArray(); $queryResult = DB::select( - /** @lang PostgreSQL */ ' + /** @lang PostgreSQL */ ' SELECT is_nullable AS nullable, column_name AS name, @@ -918,7 +923,7 @@ protected function databaseDefaultFieldsForPostgres() ->toArray(); $queryResult = DB::select( - /** @lang PostgreSQL */ ' + /** @lang PostgreSQL */ ' SELECT is_nullable AS nullable, column_name AS name, @@ -937,7 +942,7 @@ protected function databaseDefaultFieldsForPostgres() return (array) $column; }) ->filter(function ($column) use ($primaryIndex) { - return $column['default'] !== null && ! (in_array($column['name'], $primaryIndex)); + return $column['default'] !== null && !(in_array($column['name'], $primaryIndex)); }) ->pluck('name') ->unique() @@ -977,7 +982,7 @@ protected function requiredFieldsForSqlServer() ->toArray(); $queryResult = DB::select( - /** @lang TSQL */ " + /** @lang TSQL */ " SELECT COLUMN_NAME AS name, DATA_TYPE AS type, @@ -1018,7 +1023,7 @@ protected function databaseDefaultFieldsForSqlServer() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang TSQL */ " + /** @lang TSQL */ " SELECT COLUMN_NAME AS name, DATA_TYPE AS type, @@ -1080,7 +1085,7 @@ protected function allFieldsForSqlServer() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang TSQL */ " + /** @lang TSQL */ " SELECT COLUMN_NAME AS name, DATA_TYPE AS type, @@ -1112,7 +1117,7 @@ protected function nullableFieldsForSqlServer() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang TSQL */ " + /** @lang TSQL */ " SELECT COLUMN_NAME AS name, DATA_TYPE AS type, diff --git a/src/ModelFieldsServiceProvider.php b/src/ModelFieldsServiceProvider.php index de754a8..53b69a8 100644 --- a/src/ModelFieldsServiceProvider.php +++ b/src/ModelFieldsServiceProvider.php @@ -169,6 +169,7 @@ public function boot() $table = Helpers::getTableFromThisModel($this->getModel()); $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->getModel()); + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->getModel()); $primaryIndex = $this->primaryField(); @@ -191,6 +192,9 @@ public function boot() ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); }) + ->reject(function ($column) use ($observerDefaultAttributes) { + return in_array($column['name'], $observerDefaultAttributes); + }) ->pluck('name') ->unique() ->values() diff --git a/src/Support/Helpers.php b/src/Support/Helpers.php index 3f73dc7..af5de57 100644 --- a/src/Support/Helpers.php +++ b/src/Support/Helpers.php @@ -4,9 +4,10 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Event; /** - * Here are the shared logic across multiple files, now are ModelFieldsService & ModelFieldsServiceProvider + * Here are the shared logic across multiple files, now are FieldsService & ModelFieldsServiceProvider */ class Helpers { @@ -38,100 +39,29 @@ public static function getTableFromThisModel($model) * during 'creating' and 'saving' events * * @param class-string $model - * @return string[] - */ - public static function getObserverFilledFields($model) - { - /** @var Model $modelInstance */ - $modelInstance = new $model; - - // Get attributes before firing events - $attributesBeforeEvents = array_keys($modelInstance->getAttributes()); - - // Fire the creating and saving events to trigger observers - $modelInstance->fireModelEvent('creating', false); - $modelInstance->fireModelEvent('saving', false); - - // Get attributes after firing events - $attributesAfterEvents = array_keys($modelInstance->getAttributes()); - - // Get all database fields - $allFields = self::getAllFieldsForModel($model); - - // Return only the new fields that were added by observers/events - // and are actual database fields - return collect($attributesAfterEvents) - ->diff($attributesBeforeEvents) - ->filter(function ($field) use ($allFields) { - return in_array($field, $allFields); - }) - ->values() - ->toArray(); - } - - /** - * Helper to get all fields for a model (used internally) * - * @param class-string $model * @return string[] */ - private static function getAllFieldsForModel($model) + public static function getObserverFilledFields( $modelOrClass) { - if (self::isLaravelVersionLessThan10()) { - // For older versions, we need to query the database - $table = self::getTableFromThisModel($model); - $databaseDriver = \Illuminate\Support\Facades\DB::connection()->getDriverName(); - - switch ($databaseDriver) { - case 'sqlite': - $queryResult = \Illuminate\Support\Facades\DB::select("PRAGMA table_info($table)"); - - return collect($queryResult) - ->map(fn ($column) => (array) $column) - ->pluck('name') - ->toArray(); - case 'mysql': - case 'mariadb': - $queryResult = \Illuminate\Support\Facades\DB::select( - 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION ASC', - [$table] - ); - - return collect($queryResult) - ->map(fn ($column) => (array) $column) - ->pluck('name') - ->toArray(); - case 'pgsql': - $queryResult = \Illuminate\Support\Facades\DB::select( - 'SELECT column_name AS name FROM information_schema.columns WHERE table_name = ? ORDER BY ordinal_position ASC', - [$table] - ); + if ($modelOrClass instanceof Model) { + $model = $modelOrClass->newInstance(); // fresh instance of same model + $modelClass = get_class($modelOrClass); + } else { + $model = new $modelOrClass; + $modelClass = $modelOrClass; + } - return collect($queryResult) - ->map(fn ($column) => (array) $column) - ->pluck('name') - ->toArray(); - case 'sqlsrv': - $queryResult = \Illuminate\Support\Facades\DB::select( - 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = SCHEMA_NAME() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION ASC', - [$table] - ); + // ensure clean baseline + $model->syncOriginal(); - return collect($queryResult) - ->map(fn ($column) => (array) $column) - ->pluck('name') - ->toArray(); - default: - return []; - } - } + // fire the creating events (observer + model booted events) + Event::dispatch("eloquent.creating: {$modelClass}", $model); + Event::dispatch("eloquent.saving: {$modelClass}", $model); - $table = self::getTableFromThisModel($model); + $dirty = $model->getDirty(); + $dirtyNoNull = array_filter($dirty); // exclude null values - return collect(\Illuminate\Support\Facades\Schema::getColumns($table)) - ->pluck('name') - ->unique() - ->values() - ->toArray(); + return array_keys($dirtyNoNull); } } diff --git a/tests/FieldsTest.php b/tests/FieldsTest.php index 6d00ac7..b3270b7 100644 --- a/tests/FieldsTest.php +++ b/tests/FieldsTest.php @@ -45,8 +45,7 @@ public function test_macro_is_overridden_when_same_static_method_name_added() $table->timestamps(); }); - $testModelClass = new class extends Model - { + $testModelClass = new class extends Model { protected $table = 'test_table'; public static function requiredFields() @@ -220,6 +219,14 @@ public function test_required_fields_for_brother_model() $this->assertEquals($expected, Brother::requiredFieldsForOlderVersions()); } + public function test_required_fields_for_uncles_model() + { + $expected = []; + + $this->assertEquals($expected, Fields::model(Uncle::class)->requiredFields()); + $this->assertEquals($expected, Uncle::requiredFields()); + } + public function test_nullable_fields_for_father_model() { $expected = [ @@ -375,16 +382,6 @@ public function test_default_fields_for_brother_model() $this->assertEquals($expected, Brother::defaultFields()); } - public function test_required_fields_for_uncles_model() - { - $expected = [ - 'column_2', - ]; - - $this->assertEquals($expected, Fields::model(Uncle::class)->requiredFields()); - //$this->assertEquals($expected, Uncle::defaultFields()); - } - /** * @throws ReflectionException */ diff --git a/tests/Listeners/UncleCreatingListener.php b/tests/Listeners/UncleCreatingListener.php new file mode 100644 index 0000000..08dcedf --- /dev/null +++ b/tests/Listeners/UncleCreatingListener.php @@ -0,0 +1,13 @@ +model->event_creating = 'creating'; + } +} diff --git a/tests/Listeners/UncleSavingListener.php b/tests/Listeners/UncleSavingListener.php new file mode 100644 index 0000000..0d65494 --- /dev/null +++ b/tests/Listeners/UncleSavingListener.php @@ -0,0 +1,13 @@ +model->event_saving = 'saving'; + } +} diff --git a/tests/Models/Uncle.php b/tests/Models/Uncle.php index a776a12..93776fd 100644 --- a/tests/Models/Uncle.php +++ b/tests/Models/Uncle.php @@ -4,26 +4,56 @@ use Illuminate\Database\Eloquent\Model; -class Uncle extends Model { +class Uncle extends Model +{ + protected $dispatchesEvents = [ + 'creating' => UncleCreating::class, // fill `event_creating` field + 'saving' => UncleSaving::class, // fill `event_saving` field + ]; - // todo check if it works - //protected $dispatchesEvents = [ - // 'saved' => UserSaved::class, - // 'deleted' => UserDeleted::class, - //]; protected static function boot(): void { parent::boot(); + self::observe(UncleObserver::class); + self::creating(function ($model) { - $model->column_1 = 'creating'; + $model->boot_creating = 'creating'; }); self::saving(function ($model) { - // todo if model is new then it will be created - $model->column_1 = 'created'; + $model->boot_saving = 'saving'; }); } } -// todo UncleObserver +class UncleObserver +{ + public function creating(Uncle $model): void + { + $model->observer_creating = 'creating'; + } + + public function saving(Uncle $model): void + { + $model->observer_saving = 'saving'; + } +} + +class UncleCreating +{ + public Uncle $model; + + public function __construct(Uncle $model) { + $this->model = $model; + } +} + +class UncleSaving +{ + public Uncle $model; + + public function __construct(Uncle $model) { + $this->model = $model; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index e6b4793..00c084e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,14 +2,37 @@ namespace WatheqAlshowaiter\ModelFields\Tests; +use Illuminate\Support\Facades\Event; use Orchestra\Testbench\TestCase as Orchestra; use WatheqAlshowaiter\ModelFields\ModelFieldsServiceProvider; +use WatheqAlshowaiter\ModelFields\Tests\Listeners\UncleCreatingListener; +use WatheqAlshowaiter\ModelFields\Tests\Listeners\UncleSavingListener; +use WatheqAlshowaiter\ModelFields\Tests\Models\UncleCreating; +use WatheqAlshowaiter\ModelFields\Tests\Models\UncleSaving; class TestCase extends Orchestra { protected function setUp(): void { parent::setUp(); + + // Register event listeners for Uncle model events + Event::listen(UncleCreating::class, UncleCreatingListener::class); + Event::listen(UncleSaving::class, UncleSavingListener::class); + + // Register listeners for the eloquent events that the package uses for detection + Event::listen( + 'eloquent.creating: WatheqAlshowaiter\ModelFields\Tests\Models\Uncle', + function ($model) { + (new UncleCreatingListener())->handle(new UncleCreating($model)); + } + ); + Event::listen( + 'eloquent.saving: WatheqAlshowaiter\ModelFields\Tests\Models\Uncle', + function ($model) { + (new UncleSavingListener())->handle(new UncleSaving($model)); + } + ); } protected function getPackageProviders($app) diff --git a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php index ee9fbf9..fe96d40 100644 --- a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php +++ b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php @@ -11,8 +11,12 @@ class CreateUnclesTable extends Migration public function up(): void { Schema::create('uncles', function (Blueprint $table) { - $table->string('column_1'); // add by creating boot method in model - $table->string('column_2'); //added by observer creating boot method in model + $table->string('boot_creating'); // filed by creating boot method in model + $table->string('boot_saving'); //filed by saving boot method in model + $table->string('observer_creating'); // filed by observer creating + $table->string('observer_saving'); // filled by observer saving + $table->string('event_creating'); // filled by event creating + $table->string('event_saving'); // filled by event saving }); } diff --git a/todos.md b/todos.md index fc96895..0936275 100644 --- a/todos.md +++ b/todos.md @@ -67,5 +67,5 @@ - [x] test opening the url (mocking with reflectionClass) - [x] make sure $this->input->isInteractive() it enough - [ ] add new changes in using the terminal to the docs -- [ ] (next version) exclude from required fields that are filled in "creating/saving" observers/events and add test cases for +- [x] exclude from required fields that are filled in "creating/saving" observers/events and add test cases for that From a39f0182dcc80584e8e81ad140a13e204beda05e Mon Sep 17 00:00:00 2001 From: WatheqAlshowaiter <24838274+WatheqAlshowaiter@users.noreply.github.com> Date: Fri, 7 Nov 2025 19:21:02 +0000 Subject: [PATCH 03/11] Fix styling --- src/FieldsService.php | 37 +++++++++---------- src/Support/Helpers.php | 3 +- tests/FieldsTest.php | 3 +- tests/Models/Uncle.php | 6 ++- tests/TestCase.php | 4 +- .../2025_11_07_100247_create_uncles_table.php | 3 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/FieldsService.php b/src/FieldsService.php index f847fb3..2064a67 100644 --- a/src/FieldsService.php +++ b/src/FieldsService.php @@ -20,12 +20,11 @@ class FieldsService * Set up the model class to get fields from * * @param class-string $modelClass - * * @return $this */ public function model($modelClass) { - if (!$this->isEloquentModelClass($modelClass)) { + if (! $this->isEloquentModelClass($modelClass)) { throw new InvalidModelClassException('Model class must be an instance of Eloquent model'); } @@ -197,7 +196,7 @@ public function databaseDefaultFields() return $column; }) ->filter(function ($column) use ($primaryField) { - return $column['default'] !== null && !(in_array($column['name'], $primaryField)); + return $column['default'] !== null && ! (in_array($column['name'], $primaryField)); }) ->pluck('name') ->unique() @@ -389,7 +388,7 @@ protected function requiredFieldsForSqlite() ->reject(function ($column) { return $column['pk'] || $column['dflt_value'] != null - || !$column['notnull']; + || ! $column['notnull']; }) ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); @@ -468,7 +467,7 @@ protected function nullableFieldsForSqlite() return (array) $column; }) ->filter(function ($column) { - return !$column['notnull']; + return ! $column['notnull']; }) ->pluck('name') ->toArray(); @@ -488,7 +487,7 @@ protected function requiredFieldsForMysqlAndMariaDb() $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->modelClass); $queryResult = DB::select( - /** @lang SQLite */ " + /** @lang SQLite */ " SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type, @@ -536,7 +535,7 @@ protected function databaseDefaultFieldsForMysqlAndMariaDb() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang SQLite */ " + /** @lang SQLite */ " SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type, @@ -579,7 +578,7 @@ protected function primaryFieldForMysqlAndMariaDb() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang SQLite */ " + /** @lang SQLite */ " SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type, @@ -622,7 +621,7 @@ protected function allFieldsForMysqlAndMariaDb() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang SQLite */ ' + /** @lang SQLite */ ' SELECT COLUMN_NAME AS name FROM @@ -651,7 +650,7 @@ protected function nullableFieldsForMysqlAndMariaDb() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang SQLite */ " + /** @lang SQLite */ " SELECT COLUMN_NAME AS name, COLUMN_TYPE AS type, @@ -745,7 +744,7 @@ protected function allFieldsForPostgres() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang PostgreSQL */ ' + /** @lang PostgreSQL */ ' SELECT is_nullable AS nullable, column_name AS name, @@ -776,7 +775,7 @@ protected function nullableFieldsForPostgres() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang PostgreSQL */ ' + /** @lang PostgreSQL */ ' SELECT is_nullable AS nullable, column_name AS name @@ -847,7 +846,7 @@ protected function requiredFieldsForPostgres() ->toArray(); $queryResult = DB::select( - /** @lang PostgreSQL */ ' + /** @lang PostgreSQL */ ' SELECT is_nullable AS nullable, column_name AS name, @@ -923,7 +922,7 @@ protected function databaseDefaultFieldsForPostgres() ->toArray(); $queryResult = DB::select( - /** @lang PostgreSQL */ ' + /** @lang PostgreSQL */ ' SELECT is_nullable AS nullable, column_name AS name, @@ -942,7 +941,7 @@ protected function databaseDefaultFieldsForPostgres() return (array) $column; }) ->filter(function ($column) use ($primaryIndex) { - return $column['default'] !== null && !(in_array($column['name'], $primaryIndex)); + return $column['default'] !== null && ! (in_array($column['name'], $primaryIndex)); }) ->pluck('name') ->unique() @@ -982,7 +981,7 @@ protected function requiredFieldsForSqlServer() ->toArray(); $queryResult = DB::select( - /** @lang TSQL */ " + /** @lang TSQL */ " SELECT COLUMN_NAME AS name, DATA_TYPE AS type, @@ -1023,7 +1022,7 @@ protected function databaseDefaultFieldsForSqlServer() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang TSQL */ " + /** @lang TSQL */ " SELECT COLUMN_NAME AS name, DATA_TYPE AS type, @@ -1085,7 +1084,7 @@ protected function allFieldsForSqlServer() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang TSQL */ " + /** @lang TSQL */ " SELECT COLUMN_NAME AS name, DATA_TYPE AS type, @@ -1117,7 +1116,7 @@ protected function nullableFieldsForSqlServer() $table = Helpers::getTableFromThisModel($this->modelClass); $queryResult = DB::select( - /** @lang TSQL */ " + /** @lang TSQL */ " SELECT COLUMN_NAME AS name, DATA_TYPE AS type, diff --git a/src/Support/Helpers.php b/src/Support/Helpers.php index af5de57..81aafd4 100644 --- a/src/Support/Helpers.php +++ b/src/Support/Helpers.php @@ -39,10 +39,9 @@ public static function getTableFromThisModel($model) * during 'creating' and 'saving' events * * @param class-string $model - * * @return string[] */ - public static function getObserverFilledFields( $modelOrClass) + public static function getObserverFilledFields($modelOrClass) { if ($modelOrClass instanceof Model) { $model = $modelOrClass->newInstance(); // fresh instance of same model diff --git a/tests/FieldsTest.php b/tests/FieldsTest.php index b3270b7..9c0e912 100644 --- a/tests/FieldsTest.php +++ b/tests/FieldsTest.php @@ -45,7 +45,8 @@ public function test_macro_is_overridden_when_same_static_method_name_added() $table->timestamps(); }); - $testModelClass = new class extends Model { + $testModelClass = new class extends Model + { protected $table = 'test_table'; public static function requiredFields() diff --git a/tests/Models/Uncle.php b/tests/Models/Uncle.php index 93776fd..3ea5426 100644 --- a/tests/Models/Uncle.php +++ b/tests/Models/Uncle.php @@ -44,7 +44,8 @@ class UncleCreating { public Uncle $model; - public function __construct(Uncle $model) { + public function __construct(Uncle $model) + { $this->model = $model; } } @@ -53,7 +54,8 @@ class UncleSaving { public Uncle $model; - public function __construct(Uncle $model) { + public function __construct(Uncle $model) + { $this->model = $model; } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 00c084e..af204c4 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -24,13 +24,13 @@ protected function setUp(): void Event::listen( 'eloquent.creating: WatheqAlshowaiter\ModelFields\Tests\Models\Uncle', function ($model) { - (new UncleCreatingListener())->handle(new UncleCreating($model)); + (new UncleCreatingListener)->handle(new UncleCreating($model)); } ); Event::listen( 'eloquent.saving: WatheqAlshowaiter\ModelFields\Tests\Models\Uncle', function ($model) { - (new UncleSavingListener())->handle(new UncleSaving($model)); + (new UncleSavingListener)->handle(new UncleSaving($model)); } ); } diff --git a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php index fe96d40..a2bfb73 100644 --- a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php +++ b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php @@ -5,14 +5,13 @@ use Illuminate\Support\Facades\Schema; /** @noinspection PhpIllegalPsrClassPathInspection */ - class CreateUnclesTable extends Migration { public function up(): void { Schema::create('uncles', function (Blueprint $table) { $table->string('boot_creating'); // filed by creating boot method in model - $table->string('boot_saving'); //filed by saving boot method in model + $table->string('boot_saving'); // filed by saving boot method in model $table->string('observer_creating'); // filed by observer creating $table->string('observer_saving'); // filled by observer saving $table->string('event_creating'); // filled by event creating From eae6c458479230d16cccdf5124e7f53dd394e7bb Mon Sep 17 00:00:00 2001 From: Watheq Alshowaiter Date: Sat, 8 Nov 2025 00:05:25 +0300 Subject: [PATCH 04/11] fix: add filtration of event-filled fields from required fields in other databases --- src/FieldsService.php | 16 ++++++++++++++++ src/ModelFieldsServiceProvider.php | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/FieldsService.php b/src/FieldsService.php index f847fb3..9bbfd1f 100644 --- a/src/FieldsService.php +++ b/src/FieldsService.php @@ -379,6 +379,7 @@ protected function requiredFieldsForSqlite() { $table = Helpers::getTableFromThisModel($this->modelClass); $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->modelClass); + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->modelClass); $queryResult = DB::select(/** @lang SQLite */ "PRAGMA table_info($table)"); @@ -394,6 +395,9 @@ protected function requiredFieldsForSqlite() ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); }) + ->reject(function ($column) use ($observerDefaultAttributes) { + return in_array($column['name'], $observerDefaultAttributes); + }) ->pluck('name') ->toArray(); } @@ -486,6 +490,7 @@ protected function requiredFieldsForMysqlAndMariaDb() { $table = Helpers::getTableFromThisModel($this->modelClass); $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->modelClass); + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->modelClass); $queryResult = DB::select( /** @lang SQLite */ " @@ -524,6 +529,9 @@ protected function requiredFieldsForMysqlAndMariaDb() ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); }) + ->reject(function ($column) use ($observerDefaultAttributes) { + return in_array($column['name'], $observerDefaultAttributes); + }) ->pluck('name') ->toArray(); } @@ -808,6 +816,7 @@ protected function requiredFieldsForPostgres() { $table = Helpers::getTableFromThisModel($this->modelClass); $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->modelClass); + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->modelClass); $primaryIndex = DB::select(/** @lang PostgreSQL */ " SELECT @@ -873,6 +882,9 @@ protected function requiredFieldsForPostgres() ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); }) + ->reject(function ($column) use ($observerDefaultAttributes) { + return in_array($column['name'], $observerDefaultAttributes); + }) ->pluck('name') ->unique() ->toArray(); @@ -961,6 +973,7 @@ protected function requiredFieldsForSqlServer() { $table = Helpers::getTableFromThisModel($this->modelClass); $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->modelClass); + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->modelClass); $primaryIndex = DB::select(/** @lang TSQL */ ' SELECT @@ -1011,6 +1024,9 @@ protected function requiredFieldsForSqlServer() ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); }) + ->reject(function ($column) use ($observerDefaultAttributes) { + return in_array($column['name'], $observerDefaultAttributes); + }) ->pluck('name') ->toArray(); } diff --git a/src/ModelFieldsServiceProvider.php b/src/ModelFieldsServiceProvider.php index 53b69a8..64ae058 100644 --- a/src/ModelFieldsServiceProvider.php +++ b/src/ModelFieldsServiceProvider.php @@ -759,6 +759,7 @@ public function boot() Builder::macro('requiredFieldsForSqlite', function () { $table = Helpers::getTableFromThisModel($this->getModel()); $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->getModel()); + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->getModel()); $queryResult = DB::select(/** @lang SQLite */ "PRAGMA table_info($table)"); @@ -774,6 +775,9 @@ public function boot() ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); }) + ->reject(function ($column) use ($observerDefaultAttributes) { + return in_array($column['name'], $observerDefaultAttributes); + }) ->pluck('name') ->toArray(); }); @@ -781,6 +785,7 @@ public function boot() Builder::macro('requiredFieldsForMysqlAndMariaDb', function () { $table = Helpers::getTableFromThisModel($this->getModel()); $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->getModel()); + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->getModel()); $queryResult = DB::select( /** @lang SQLite */ " @@ -819,6 +824,9 @@ public function boot() ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); }) + ->reject(function ($column) use ($observerDefaultAttributes) { + return in_array($column['name'], $observerDefaultAttributes); + }) ->pluck('name') ->toArray(); }); @@ -826,6 +834,7 @@ public function boot() Builder::macro('requiredFieldsForPostgres', function () { $table = Helpers::getTableFromThisModel($this->getModel()); $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->getModel()); + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->getModel()); $primaryIndex = DB::select(/** @lang PostgreSQL */ " SELECT @@ -891,6 +900,9 @@ public function boot() ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); }) + ->reject(function ($column) use ($observerDefaultAttributes) { + return in_array($column['name'], $observerDefaultAttributes); + }) ->pluck('name') ->unique() ->toArray(); @@ -899,6 +911,7 @@ public function boot() Builder::macro('requiredFieldsForSqlServer', function () { $table = Helpers::getTableFromThisModel($this->getModel()); $modelDefaultAttributes = Helpers::getModelDefaultAttributes($this->getModel()); + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->getModel()); $primaryIndex = DB::select(/** @lang TSQL */ ' SELECT @@ -949,6 +962,9 @@ public function boot() ->reject(function ($column) use ($modelDefaultAttributes) { return in_array($column['name'], $modelDefaultAttributes); }) + ->reject(function ($column) use ($observerDefaultAttributes) { + return in_array($column['name'], $observerDefaultAttributes); + }) ->pluck('name') ->toArray(); }); From 155bb58d529aa4facf5aece912bd78c80a52d1cc Mon Sep 17 00:00:00 2001 From: Watheq Alshowaiter Date: Sat, 8 Nov 2025 00:10:06 +0300 Subject: [PATCH 05/11] test: improve test cases by adding older versions --- tests/FieldsTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/FieldsTest.php b/tests/FieldsTest.php index b3270b7..85af5ed 100644 --- a/tests/FieldsTest.php +++ b/tests/FieldsTest.php @@ -224,7 +224,9 @@ public function test_required_fields_for_uncles_model() $expected = []; $this->assertEquals($expected, Fields::model(Uncle::class)->requiredFields()); + $this->assertEquals($expected, Fields::model(Uncle::class)->requiredFieldsForOlderVersions()); $this->assertEquals($expected, Uncle::requiredFields()); + $this->assertEquals($expected, Uncle::requiredFieldsForOlderVersions()); } public function test_nullable_fields_for_father_model() From 3143fbd6b2bef9102354e1861cea9fdf0d93651a Mon Sep 17 00:00:00 2001 From: Watheq Alshowaiter Date: Sat, 8 Nov 2025 00:10:20 +0300 Subject: [PATCH 06/11] chore: fix typos --- .../migrations/2025_11_07_100247_create_uncles_table.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php index fe96d40..8b8907f 100644 --- a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php +++ b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php @@ -11,9 +11,9 @@ class CreateUnclesTable extends Migration public function up(): void { Schema::create('uncles', function (Blueprint $table) { - $table->string('boot_creating'); // filed by creating boot method in model - $table->string('boot_saving'); //filed by saving boot method in model - $table->string('observer_creating'); // filed by observer creating + $table->string('boot_creating'); // filled by creating boot method in model + $table->string('boot_saving'); // filled by saving boot method in model + $table->string('observer_creating'); // filled by observer creating $table->string('observer_saving'); // filled by observer saving $table->string('event_creating'); // filled by event creating $table->string('event_saving'); // filled by event saving From 8784d5a5e6f08dbc2050b7b45f2c38691502ca30 Mon Sep 17 00:00:00 2001 From: WatheqAlshowaiter <24838274+WatheqAlshowaiter@users.noreply.github.com> Date: Fri, 7 Nov 2025 21:13:06 +0000 Subject: [PATCH 07/11] Fix styling --- .../migrations/2025_11_07_100247_create_uncles_table.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php index 8b8907f..9e6d354 100644 --- a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php +++ b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php @@ -5,7 +5,6 @@ use Illuminate\Support\Facades\Schema; /** @noinspection PhpIllegalPsrClassPathInspection */ - class CreateUnclesTable extends Migration { public function up(): void From 4f67debf200ee89d0cdee134f87368d0a6609a27 Mon Sep 17 00:00:00 2001 From: Watheq Alshowaiter Date: Sat, 8 Nov 2025 00:40:07 +0300 Subject: [PATCH 08/11] docs(readme): document observer and event-filled fields - Added new section explaining how auto-filled fields are excluded from required fields - Documented three patterns: boot closures, observers, and dispatched events - Included comprehensive code example showing all patterns in action - Clarifies behavior of requiredFields() when using model event handlers --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4cd44c8..34b666e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ [link-palestine]: https://github.com/TheBSD/StandWithPalestine/blob/main/docs/README.md -Quickly retrieve **required**, **nullable**, and **default** fields for any Laravel model. Think that's simple? You probably haven’t faced the legacy projects I have. :). +Quickly retrieve **required**, **nullable**, and **default** fields for any Laravel model. Think that's simple? You +probably haven’t faced the legacy projects I have. :). > [!Note] > This is the documentation for version 3, if you want the version 1 or version 2 documentations go @@ -153,6 +154,63 @@ Post::requiredFields(); // returns ['user_id', 'ulid', 'title', 'description'] php artisan model:fields App\\Models\\Post --required # or -r ``` +#### Observer and Event-Filled Fields + +Fields that are automatically filled when creating by model observers, boot events, and event listeners are +automatically excluded from required fields. + +The package supports three patterns: + +- **Boot method closures:** `self::creating()`, `self::saving()` +- **Observer pattern:** `PostObserver` class +- **Dispatched events:** `$dispatchesEvents` property + +For example, given this model: + +```php +class Post extends Model +{ + protected $dispatchesEvents = [ + // a dispatched event trigger listener that fills the `number` field + 'creating' => PostCreatingEvent::class, + ]; + + protected static function boot() + { + parent::boot(); + self::observe(PostObserver::class); + + self::creating(function ($model) { + $model->uuid = Str::uuid(); + }); + + self::saving(function ($model) { + $model->ulid = Str::ulid(); + }); + } +} + +class PostObserver +{ + public function creating(Post $model): void + { + $model->status = 'draft'; + } + + public function saving(Post $model): void + { + $model->state = 'pending'; + } +} +``` + +```php +Post::requiredFields(); + +// returns only user-provided fields, excluding auto-filled ones like: +// `uuid`, `ulid`, `status`, `state`, and `number` +``` + ### And more We have the flexibility to get all fields, required fields, nullable fields, primary key, database default fields, @@ -307,7 +365,7 @@ php artisan model:fields \\Modules\\Order\\src\\Models\\Order php artisan model:fields "Modules\Order\src\Models\Order" ``` -- You have 3 output formats: list, json and table. the list is the default +- You have 3 output formats: list, json, and table. the list is the default ```sh php artisan model:fields User --format=json @@ -378,8 +436,10 @@ them. ## Related Packages -- **[Backup Tables](https://github.com/WatheqAlshowaiter/backup-tables)** - Backup single or multiple database tables with ease. -- **[Filament Sticky Table Header](https://github.com/WatheqAlshowaiter/filament-sticky-table-header)** - Make Filament table headers stick when scrolling for better UX. +- **[Backup Tables](https://github.com/WatheqAlshowaiter/backup-tables)** - Backup single or multiple database tables + with ease. +- **[Filament Sticky Table Header](https://github.com/WatheqAlshowaiter/filament-sticky-table-header)** - Make Filament + table headers stick when scrolling for better UX. ## Credits From 3832f365d77423ee847a4452dfb8b9be4b4347c6 Mon Sep 17 00:00:00 2001 From: Watheq Alshowaiter Date: Sat, 8 Nov 2025 02:00:19 +0300 Subject: [PATCH 09/11] feat(fields): auto-exclude event-filled fields from required fields - Implement detection of fields auto-filled by observers, boot events, and dispatched events - Add getObserverFilledFields() helper to capture dynamically filled fields - Update requiredFields() across all database drivers to exclude observer-filled fields - Add comprehensive documentation with examples of all three auto-fill patterns - Add test models (Brother, Uncle) demonstrating the new feature --- README.md | 65 +++++++++++++++---- src/FieldsService.php | 5 +- src/ModelFieldsServiceProvider.php | 5 +- src/Support/Helpers.php | 3 +- tests/FieldsTest.php | 39 +++++++++-- tests/Models/Brother.php | 1 + tests/Models/Uncle.php | 4 ++ ...025_02_14_100247_create_brothers_table.php | 1 + .../2025_11_07_100247_create_uncles_table.php | 1 + 9 files changed, 104 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 34b666e..706e56e 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ For example, given this model: class Post extends Model { protected $dispatchesEvents = [ - // a dispatched event trigger listener that fills the `number` field + // a dispatched event trigger listener that fills the `user_id` field 'creating' => PostCreatingEvent::class, ]; @@ -181,11 +181,11 @@ class Post extends Model self::observe(PostObserver::class); self::creating(function ($model) { - $model->uuid = Str::uuid(); + $model->ulid = Str::ulid(); }); self::saving(function ($model) { - $model->ulid = Str::ulid(); + $model->title = 'default title' }); } } @@ -194,12 +194,12 @@ class PostObserver { public function creating(Post $model): void { - $model->status = 'draft'; + $model->description = 'default description'; } public function saving(Post $model): void { - $model->state = 'pending'; + $model->description = 'default saving description'; } } ``` @@ -207,8 +207,7 @@ class PostObserver ```php Post::requiredFields(); -// returns only user-provided fields, excluding auto-filled ones like: -// `uuid`, `ulid`, `status`, `state`, and `number` +// returns [] because it excludes auto-filled fields ``` ### And more @@ -294,18 +293,54 @@ Fields::model(Post::class)->applicationDefaultFields(); //or Post::applicationDefaultFields(); -// If there is default attributes in the model +// If there are default attributes in the model class Post extends Model { protected $attributes = [ 'title' => 'default title', - 'description' => 'default description', + 'description' => null, // will be ignored + ]; + + protected $dispatchesEvents = [ + // if there is a field autofilled by this event, + // then it will be added to the application default fields + 'creating' => PostCreatingEvent::class, ]; + + // or any event-filled fields + protected static function boot(): void + { + parent::boot(); + self::observe(PostObserver::class); + + self::creating(function ($model) { + $model->uuid = Str::uuid(); + }); + + self::saving(function ($model) { + $model->ulid = Str::ulid(); + }); + } +} + +// the same in the observer class +class PostObserver +{ + + public function creating(Post $model): void + { + // .. + } + + public function saving(Post $model): void + { + // .. + } } // returns // [ -// 'title', 'description', +// 'title', 'uuid', 'ulid', // ] ``` @@ -327,8 +362,16 @@ class Post extends Model { protected $attributes = [ 'title' => 'default title', - 'description' => 'default description', ]; + + protected static function boot(): void + { + parent::boot(); + + self::creating(function ($model) { + $model->description = 'default description'; + }); + } } // returns diff --git a/src/FieldsService.php b/src/FieldsService.php index c132e7b..b19ed03 100644 --- a/src/FieldsService.php +++ b/src/FieldsService.php @@ -260,12 +260,13 @@ public function applicationDefaultFields() $this->throwIfNotUsingModelMethodFirst(); $modelInstance = new $this->modelClass; - $attributes = $modelInstance->getAttributes(); + $attributes = collect($modelInstance->getAttributes())->filter()->keys()->toArray(); // ignore null values + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->modelClass); $allFields = $this->allFields(); return collect($attributes) - ->keys() + ->merge($observerDefaultAttributes) ->filter(function ($field) use ($allFields) { return in_array($field, $allFields); }) diff --git a/src/ModelFieldsServiceProvider.php b/src/ModelFieldsServiceProvider.php index 64ae058..3c5e599 100644 --- a/src/ModelFieldsServiceProvider.php +++ b/src/ModelFieldsServiceProvider.php @@ -594,12 +594,13 @@ public function boot() Builder::macro('applicationDefaultFields', function () { $modelClass = $this->getModel(); $modelInstance = new $modelClass; - $attributes = $modelInstance->getAttributes(); + $attributes = collect($modelInstance->getAttributes())->filter()->keys()->toArray(); // ignore null values + $observerDefaultAttributes = Helpers::getObserverFilledFields($this->getModel()); $allFields = $this->allFields(); return collect($attributes) - ->keys() + ->merge($observerDefaultAttributes) ->filter(function ($field) use ($allFields) { return in_array($field, $allFields); }) diff --git a/src/Support/Helpers.php b/src/Support/Helpers.php index 81aafd4..b8a259f 100644 --- a/src/Support/Helpers.php +++ b/src/Support/Helpers.php @@ -38,7 +38,8 @@ public static function getTableFromThisModel($model) * Get fields that are automatically filled by model observers/events * during 'creating' and 'saving' events * - * @param class-string $model + * @param $modelOrClass + * * @return string[] */ public static function getObserverFilledFields($modelOrClass) diff --git a/tests/FieldsTest.php b/tests/FieldsTest.php index b7b4128..c98186c 100644 --- a/tests/FieldsTest.php +++ b/tests/FieldsTest.php @@ -45,8 +45,7 @@ public function test_macro_is_overridden_when_same_static_method_name_added() $table->timestamps(); }); - $testModelClass = new class extends Model - { + $testModelClass = new class extends Model { protected $table = 'test_table'; public static function requiredFields() @@ -220,7 +219,7 @@ public function test_required_fields_for_brother_model() $this->assertEquals($expected, Brother::requiredFieldsForOlderVersions()); } - public function test_required_fields_for_uncles_model() + public function test_required_fields_for_uncle_model() { $expected = []; @@ -356,6 +355,22 @@ public function test_application_default_fields_for_brother_model() $this->assertEquals($expected, Brother::applicationDefaultFields()); } + public function test_application_default_fields_for_uncle_model() + { + $expected = [ + 'attribute_field', + 'event_creating', + 'observer_creating', + 'boot_creating', + 'event_saving', + 'observer_saving', + 'boot_saving', + ]; + + $this->assertEquals($expected, Fields::model(Uncle::class)->applicationDefaultFields()); + $this->assertEquals($expected, Uncle::applicationDefaultFields()); + } + public function test_default_fields_for_mother_model() { $expected = [ @@ -385,12 +400,28 @@ public function test_default_fields_for_brother_model() $this->assertEquals($expected, Brother::defaultFields()); } + public function test_default_fields_for_uncle_model() + { + $expected = [ + 'attribute_field', + 'event_creating', + 'observer_creating', + 'boot_creating', + 'event_saving', + 'observer_saving', + 'boot_saving', + ]; + + $this->assertEquals($expected, Fields::model(Uncle::class)->defaultFields()); + $this->assertEquals($expected, Uncle::defaultFields()); + } + /** * @throws ReflectionException */ private function removeMacro(string $class, string $macro): void { - if (! method_exists($class, 'hasMacro')) { + if (!method_exists($class, 'hasMacro')) { return; } diff --git a/tests/Models/Brother.php b/tests/Models/Brother.php index c7df1e3..a4ad4ea 100644 --- a/tests/Models/Brother.php +++ b/tests/Models/Brother.php @@ -11,5 +11,6 @@ class Brother extends Model 'another' => '', // non-valid default 'number' => '0000', // default for nullable field 'non-existed-field' => 'some-random-value', // non-existed field in the database + 'address' => null, // non useful default - should be ignored ]; } diff --git a/tests/Models/Uncle.php b/tests/Models/Uncle.php index 3ea5426..fbcf78c 100644 --- a/tests/Models/Uncle.php +++ b/tests/Models/Uncle.php @@ -6,6 +6,10 @@ class Uncle extends Model { + protected $attributes = [ + 'attribute_field' => 'default-value', + ]; + protected $dispatchesEvents = [ 'creating' => UncleCreating::class, // fill `event_creating` field 'saving' => UncleSaving::class, // fill `event_saving` field diff --git a/tests/database/migrations/2025_02_14_100247_create_brothers_table.php b/tests/database/migrations/2025_02_14_100247_create_brothers_table.php index 86bd79d..a1f0a61 100644 --- a/tests/database/migrations/2025_02_14_100247_create_brothers_table.php +++ b/tests/database/migrations/2025_02_14_100247_create_brothers_table.php @@ -13,6 +13,7 @@ public function up(): void $table->string('email'); // required $table->string('name'); // default => ignored because it has the default value in the model $attributes $table->string('number')->nullable(); // nullable, with default in model + $table->string('address'); }); } diff --git a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php index 9e6d354..7df9e1f 100644 --- a/tests/database/migrations/2025_11_07_100247_create_uncles_table.php +++ b/tests/database/migrations/2025_11_07_100247_create_uncles_table.php @@ -16,6 +16,7 @@ public function up(): void $table->string('observer_saving'); // filled by observer saving $table->string('event_creating'); // filled by event creating $table->string('event_saving'); // filled by event saving + $table->string('attribute_field'); // filled by event saving }); } From c2ea4a18492c278cd965bc58f85325bdc38624a6 Mon Sep 17 00:00:00 2001 From: WatheqAlshowaiter <24838274+WatheqAlshowaiter@users.noreply.github.com> Date: Fri, 7 Nov 2025 23:00:46 +0000 Subject: [PATCH 10/11] Fix styling --- src/Support/Helpers.php | 1 - tests/FieldsTest.php | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Support/Helpers.php b/src/Support/Helpers.php index b8a259f..d3926db 100644 --- a/src/Support/Helpers.php +++ b/src/Support/Helpers.php @@ -38,7 +38,6 @@ public static function getTableFromThisModel($model) * Get fields that are automatically filled by model observers/events * during 'creating' and 'saving' events * - * @param $modelOrClass * * @return string[] */ diff --git a/tests/FieldsTest.php b/tests/FieldsTest.php index c98186c..0c28abc 100644 --- a/tests/FieldsTest.php +++ b/tests/FieldsTest.php @@ -45,7 +45,8 @@ public function test_macro_is_overridden_when_same_static_method_name_added() $table->timestamps(); }); - $testModelClass = new class extends Model { + $testModelClass = new class extends Model + { protected $table = 'test_table'; public static function requiredFields() @@ -421,7 +422,7 @@ public function test_default_fields_for_uncle_model() */ private function removeMacro(string $class, string $macro): void { - if (!method_exists($class, 'hasMacro')) { + if (! method_exists($class, 'hasMacro')) { return; } From fe925e972ee9a1ee62fb8972cf17900fd9eccc3b Mon Sep 17 00:00:00 2001 From: WatheqAlshowaiter <24838274+WatheqAlshowaiter@users.noreply.github.com> Date: Fri, 7 Nov 2025 23:04:38 +0000 Subject: [PATCH 11/11] Update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1142ff..988ee6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to `model-required-fields` will be documented in this file. +## 3.2.0 - 2025-11-07 + +### What's Changed + +* Exclude event filled fields by model observers, boot events, and event listeners to know really what are required fields and what are application defatult fields by @WatheqAlshowaiter in https://github.com/WatheqAlshowaiter/model-fields/pull/36 + +**Full Changelog**: https://github.com/WatheqAlshowaiter/model-fields/compare/3.1.8...3.2.0 + ## 3.1.8 - 2025-10-05 * Update Readme