diff --git a/README.md b/README.md index ae90e68b..4a8dc454 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ -# Reliese Laravel +# Reliese Laravel Model Generator [![Build Status](https://travis-ci.org/reliese/laravel.svg?branch=master)](https://travis-ci.org/reliese/laravel) [![Latest Stable Version](https://poser.pugx.org/reliese/laravel/v/stable)](https://packagist.org/packages/reliese/laravel) [![Total Downloads](https://poser.pugx.org/reliese/laravel/downloads)](https://packagist.org/packages/reliese/laravel) [![Latest Unstable Version](https://poser.pugx.org/reliese/laravel/v/unstable)](https://packagist.org/packages/reliese/laravel) [![License](https://poser.pugx.org/reliese/laravel/license)](https://packagist.org/packages/reliese/laravel) -Reliese Laravel is a collection of Laravel Components which aim is -to help the development process of Laravel applications by -providing some convenient code-generation capabilities. +Reliese Laravel Model Generator aims to speed up the development process of Laravel applications by +providing some convenient code-generation capabilities. +The tool inspects your database structure, including column names and foreign keys, in order +to automatically generate Models that have correctly typed properties, along with any relationships to other Models. ## How does it work? @@ -22,28 +23,19 @@ It is recommended that this package should only be used on a local environment f composer require reliese/laravel --dev ``` -Then you'll need to register the provider in `app/Providers/AppServiceProvider.php` file. - -```php -public function register() -{ - if ($this->app->environment() == 'local') { - $this->app->register(\Reliese\Coders\CodersServiceProvider::class); - } -} -``` - -## Models - -![Generating models with artisan](https://cdn-images-1.medium.com/max/800/1*hOa2QxORE2zyO_-ZqJ40sA.png "Making artisan code my Eloquent models") - Add the `models.php` configuration file to your `config` directory and clear the config cache: ```shell php artisan vendor:publish --tag=reliese-models + +# Let's refresh our config cache just in case php artisan config:clear ``` +## Models + +![Generating models with artisan](https://cdn-images-1.medium.com/max/800/1*hOa2QxORE2zyO_-ZqJ40sA.png "Making artisan code my Eloquent models") + ### Usage Assuming you have already configured your database, you are now all set to go. @@ -82,7 +74,7 @@ fit your database needs. [Check it out](https://github.com/reliese/laravel/blob/ #### 1. Keeping model changes You may want to generate your models as often as you change your database. In order -not to lose you own model changes, you should set `base_files` to `true` in your `config/models.php`. +not to lose your own model changes, you should set `base_files` to `true` in your `config/models.php`. When you enable this feature your models will inherit their base configurations from base models. You should avoid adding code to your base models, since you diff --git a/composer.json b/composer.json index b7c19205..1d536f22 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=5.6.4", + "php": "^7.3|^8.0", "doctrine/dbal": ">=2.5", "illuminate/support": ">=5.1", "illuminate/database": ">=5.1", diff --git a/config/models.php b/config/models.php index e2f026e6..2f2688d4 100644 --- a/config/models.php +++ b/config/models.php @@ -211,7 +211,7 @@ | TRUE: Schema name will be prepended on the table | FALSE:Table name will be set without schema name. | NULL: Table name will follow laravel pattern, - | i.e if class name(plural) matches table name, then table name will not be added + | i.e. if class name(plural) matches table name, then table name will not be added */ 'qualified_tables' => false, @@ -253,11 +253,11 @@ | Casts |-------------------------------------------------------------------------- | - | You may want to specify which of your table fields should be casted as - | something different than a string. For instance, you may want a - | text field be casted as an array or and object. + | You may want to specify which of your table fields should be cast as + | something other than a string. For instance, you may want a + | text field be cast as an array or and object. | - | You may define column patterns which will be casted using the value + | You may define column patterns which will be cast using the value | assigned. We have defined some fields for you. Feel free to | modify them to fit your needs. | @@ -281,6 +281,10 @@ 'except' => [ 'migrations', + 'failed_jobs', + 'password_resets', + 'personal_access_tokens', + 'password_reset_tokens', ], /* @@ -321,6 +325,26 @@ 'lower_table_name_first' => false, + /* + |-------------------------------------------------------------------------- + | Model Names + |-------------------------------------------------------------------------- + | + | By default the generator will create models with names that match your tables. + | However, if you wish to manually override the naming, you can specify a mapping + | here between table and model names. + | + | Example: + | A table called 'billing_invoices' will generate a model called `BillingInvoice`, + | but you'd prefer it to generate a model called 'Invoice'. Therefore, you'd add + | the following array key and value: + | 'billing_invoices' => 'Invoice', + */ + + 'model_names' => [ + + ], + /* |-------------------------------------------------------------------------- | Relation Name Strategy @@ -333,14 +357,19 @@ generates Post::user() and User::posts() | | 'foreign_key' Use the foreign key as the relation name. - | (post.author --> user.id) - | generates Post::author() and User::posts_author() - | Column id's are ignored. + | This can help to provide more meaningful relationship names, and avoids naming conflicts + | if you have more than one relationship between two tables. | (post.author_id --> user.id) + | generates Post::author() and User::posts_where_author() + | (post.editor_id --> user.id) + | generates Post::editor() and User::posts_where_editor() + | ID suffixes can be omitted from foreign keys. + | (post.author --> user.id) + | (post.editor --> user.id) | generates the same as above. - | When the foreign key is redundant, it is omited. + | Where the foreign key matches the related table name, it behaves as per the 'related' strategy. | (post.user_id --> user.id) - | generates User::posts() and not User::posts_user() + | generates Post::user() and User::posts() */ 'relation_name_strategy' => 'related', @@ -366,6 +395,19 @@ */ 'with_property_constants' => false, + /* + |-------------------------------------------------------------------------- + | Optionally includes a full list of columns in the base generated models, + | which can be used to avoid making calls like + | + | ... + | \Illuminate\Support\Facades\Schema::getColumnListing + | ... + | + | which can be slow, especially for large tables. + */ + 'with_column_list' => false, + /* |-------------------------------------------------------------------------- | Disable Pluralization Name @@ -387,6 +429,38 @@ 'override_pluralize_for' => [ ], + + /* + |-------------------------------------------------------------------------- + | Move $hidden property to base files + |-------------------------------------------------------------------------- + | When base_files is true you can set hidden_in_base_files to true + | if you want the $hidden to be generated in base files + | + */ + 'hidden_in_base_files' => false, + + /* + |-------------------------------------------------------------------------- + | Move $fillable property to base files + |-------------------------------------------------------------------------- + | When base_files is true you can set fillable_in_base_files to true + | if you want the $fillable to be generated in base files + | + */ + 'fillable_in_base_files' => false, + + /* + |-------------------------------------------------------------------------- + | Generate return types for relation methods. + |-------------------------------------------------------------------------- + | When enable_return_types is set to true, return type declarations are added + | to all generated relation methods for your models. + | + | NOTE: This requires PHP 7.0 or later. + | + */ + 'enable_return_types' => false, ], /* diff --git a/src/Coders/Model/Factory.php b/src/Coders/Model/Factory.php index fb8db78c..84ea1936 100644 --- a/src/Coders/Model/Factory.php +++ b/src/Coders/Model/Factory.php @@ -7,12 +7,12 @@ namespace Reliese\Coders\Model; +use Illuminate\Database\DatabaseManager; +use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Str; use Reliese\Meta\Blueprint; -use Reliese\Support\Classify; use Reliese\Meta\SchemaManager; -use Illuminate\Filesystem\Filesystem; -use Illuminate\Database\DatabaseManager; +use Reliese\Support\Classify; class Factory { @@ -319,6 +319,15 @@ private function shortenAndExtractImportableDependencies(&$placeholder, $model) $namespacePieces = explode('\\', $usedClass); $className = array_pop($namespacePieces); + /** + * Avoid breaking same-model relationships when using base classes + * + * @see https://github.com/reliese/laravel/issues/209 + */ + if ($model->usesBaseFiles() && $usedClass === $model->getQualifiedUserClassName()) { + continue; + } + //When same class name but different namespace, skip it. if ( $className == $model->getClassName() && @@ -328,7 +337,7 @@ private function shortenAndExtractImportableDependencies(&$placeholder, $model) } $importableDependencies[trim($usedClass, '\\')] = true; - $placeholder = str_replace($usedClass, $className, $placeholder); + $placeholder = preg_replace('!'.addslashes($usedClass).'\b!', addslashes($className), $placeholder, 1); } } @@ -401,7 +410,8 @@ protected function body(Model $model) $properties = array_diff($properties, $excludedConstants); foreach ($properties as $property) { - $body .= $this->class->constant(strtoupper($property), $property); + $constantName = Str::upper(Str::snake($property)); + $body .= $this->class->constant($constantName, $property); } } @@ -445,19 +455,22 @@ protected function body(Model $model) $body .= $this->class->field('snakeAttributes', false, ['visibility' => 'public static']); } - if ($model->hasCasts()) { - $body .= $this->class->field('casts', $model->getCasts(), ['before' => "\n"]); + if ($model->usesColumnList()) { + $properties = array_keys($model->getProperties()); + + $body .= "\n"; + $body .= $this->class->field('columns', $properties); } - if ($model->hasDates()) { - $body .= $this->class->field('dates', $model->getDates(), ['before' => "\n"]); + if ($model->hasCasts()) { + $body .= $this->class->field('casts', $model->getCasts(), ['before' => "\n"]); } - if ($model->hasHidden() && $model->doesNotUseBaseFiles()) { + if ($model->hasHidden() && ($model->doesNotUseBaseFiles() || $model->hiddenInBaseFiles())) { $body .= $this->class->field('hidden', $model->getHidden(), ['before' => "\n"]); } - if ($model->hasFillable() && $model->doesNotUseBaseFiles()) { + if ($model->hasFillable() && ($model->doesNotUseBaseFiles() || $model->fillableInBaseFiles())) { $body .= $this->class->field('fillable', $model->getFillable(), ['before' => "\n"]); } @@ -470,7 +483,14 @@ protected function body(Model $model) } foreach ($model->getRelations() as $constraint) { - $body .= $this->class->method($constraint->name(), $constraint->body(), ['before' => "\n"]); + $body .= $this->class->method( + $constraint->name(), + $constraint->body(), + [ + 'before' => "\n", + 'returnType' => $model->definesReturnTypes() ? $constraint->returnType() : null, + ] + ); } // Make sure there not undesired line breaks @@ -562,11 +582,11 @@ protected function userFileBody(Model $model) { $body = ''; - if ($model->hasHidden()) { + if ($model->hasHidden() && !$model->hiddenInBaseFiles()) { $body .= $this->class->field('hidden', $model->getHidden()); } - if ($model->hasFillable()) { + if ($model->hasFillable() && !$model->fillableInBaseFiles()) { $body .= $this->class->field('fillable', $model->getFillable(), ['before' => "\n"]); } @@ -583,7 +603,7 @@ protected function userFileBody(Model $model) * * @return mixed|\Reliese\Coders\Model\Config */ - public function config(Blueprint $blueprint = null, $key = null, $default = null) + public function config(?Blueprint $blueprint = null, $key = null, $default = null) { if (is_null($blueprint)) { return $this->config; diff --git a/src/Coders/Model/Model.php b/src/Coders/Model/Model.php index e95eb50c..8dc54996 100644 --- a/src/Coders/Model/Model.php +++ b/src/Coders/Model/Model.php @@ -67,11 +67,6 @@ class Model */ protected $mutations = []; - /** - * @var array - */ - protected $dates = []; - /** * @var array */ @@ -162,6 +157,11 @@ class Model */ protected $relationNameStrategy = ''; + /** + * @var bool + */ + protected $definesReturnTypes = false; + /** * ModelClass constructor. * @@ -210,6 +210,8 @@ protected function configure() // Relation name settings $this->withRelationNameStrategy($this->config('relation_name_strategy', $this->getDefaultRelationNameStrategy())); + $this->definesReturnTypes = $this->config('enable_return_types', false); + return $this; } @@ -260,12 +262,8 @@ protected function parseColumn(Fluent $column) $cast = 'string'; } - // Track dates - if ($cast == 'date') { - $this->dates[] = $propertyName; - } - // Track attribute casts - elseif ($cast != 'string') { + // Track attribute casts, ignoring timestamps + if ($cast != 'string' && !in_array($propertyName, [$this->CREATED_AT, $this->UPDATED_AT])) { $this->casts[$propertyName] = $cast; } @@ -354,7 +352,7 @@ public function phpTypeHint($castType, $nullable) case 'collection': $type = '\Illuminate\Support\Collection'; break; - case 'date': + case 'datetime': $type = '\Carbon\Carbon'; break; case 'binary': @@ -521,6 +519,13 @@ public function getQualifiedUserClassName() */ public function getClassName() { + // Model names can be manually overridden by users in the config file. + // If a config entry exists for this table, use that name, rather than generating one. + $overriddenName = $this->config('model_names.' . $this->getTable()); + if ($overriddenName) { + return $overriddenName; + } + if ($this->shouldLowerCaseTableName()) { return Str::studly(Str::lower($this->getRecordName())); } @@ -987,7 +992,12 @@ public function hasDates() */ public function getDates() { - return array_diff($this->dates, [$this->CREATED_AT, $this->UPDATED_AT]); + return array_diff( + array_filter($this->casts, function (string $cast) { + return $cast === 'datetime'; + }), + [$this->CREATED_AT, $this->UPDATED_AT] + ); } /** @@ -1200,6 +1210,11 @@ public function usesPropertyConstants() return $this->config('with_property_constants', false); } + public function usesColumnList() + { + return $this->config('with_column_list', false); + } + /** * @return int */ @@ -1234,4 +1249,28 @@ public function config($key = null, $default = null) { return $this->factory->config($this->getBlueprint(), $key, $default); } + + /** + * @return bool + */ + public function fillableInBaseFiles(): bool + { + return $this->config('fillable_in_base_files', false); + } + + /** + * @return bool + */ + public function hiddenInBaseFiles(): bool + { + return $this->config('hidden_in_base_files', false); + } + + /** + * @return bool + */ + public function definesReturnTypes() + { + return $this->definesReturnTypes; + } } diff --git a/src/Coders/Model/ModelManager.php b/src/Coders/Model/ModelManager.php index 57980498..27554744 100644 --- a/src/Coders/Model/ModelManager.php +++ b/src/Coders/Model/ModelManager.php @@ -65,7 +65,7 @@ public function make($schema, $table, $mutators = [], $withRelations = true) * * @return \ArrayIterator */ - public function getIterator() + public function getIterator(): \Traversable { return new ArrayIterator($this->models); } diff --git a/src/Coders/Model/Relation.php b/src/Coders/Model/Relation.php index d61abed3..1d6bfdc0 100644 --- a/src/Coders/Model/Relation.php +++ b/src/Coders/Model/Relation.php @@ -23,4 +23,9 @@ public function name(); * @return string */ public function body(); + + /** + * @return string + */ + public function returnType(); } diff --git a/src/Coders/Model/Relations/BelongsTo.php b/src/Coders/Model/Relations/BelongsTo.php index cd0f6bf6..dba8c345 100644 --- a/src/Coders/Model/Relations/BelongsTo.php +++ b/src/Coders/Model/Relations/BelongsTo.php @@ -51,7 +51,11 @@ public function name() { switch ($this->parent->getRelationNameStrategy()) { case 'foreign_key': - $relationName = preg_replace("/[^a-zA-Z0-9]?{$this->otherKey()}$/", '', $this->foreignKey()); + $relationName = RelationHelper::stripSuffixFromForeignKey( + $this->parent->usesSnakeAttributes(), + $this->otherKey(), + $this->foreignKey() + ); break; default: case 'related': @@ -123,6 +127,14 @@ public function hint() return $base; } + /** + * @return string + */ + public function returnType() + { + return \Illuminate\Database\Eloquent\Relations\BelongsTo::class; + } + /** * @return bool */ diff --git a/src/Coders/Model/Relations/BelongsToMany.php b/src/Coders/Model/Relations/BelongsToMany.php index 7b827418..c528f5e2 100644 --- a/src/Coders/Model/Relations/BelongsToMany.php +++ b/src/Coders/Model/Relations/BelongsToMany.php @@ -136,6 +136,14 @@ public function body() return $body; } + /** + * @return string + */ + public function returnType() + { + return \Illuminate\Database\Eloquent\Relations\BelongsToMany::class; + } + /** * @return bool */ diff --git a/src/Coders/Model/Relations/HasMany.php b/src/Coders/Model/Relations/HasMany.php index f1de97a0..76c448a7 100644 --- a/src/Coders/Model/Relations/HasMany.php +++ b/src/Coders/Model/Relations/HasMany.php @@ -25,31 +25,22 @@ public function hint() */ public function name() { - if ($this->parent->shouldPluralizeTableName()) { - $relationBaseName = Str::plural(Str::singular($this->related->getTable(true))); - } else { - $relationBaseName = $this->related->getTable(true); - } - - if ($this->parent->shouldLowerCaseTableName()) { - $relationBaseName = strtolower($relationBaseName); - } - switch ($this->parent->getRelationNameStrategy()) { case 'foreign_key': - $suffix = preg_replace("/[^a-zA-Z0-9]?{$this->localKey()}$/", '', $this->foreignKey()); - - $relationName = $relationBaseName; - - // Don't make relations such as users_user, just leave it as 'users'. - if ($this->parent->getTable(true) !== $suffix) { - $relationName .= "_{$suffix}"; + $relationName = RelationHelper::stripSuffixFromForeignKey( + $this->parent->usesSnakeAttributes(), + $this->localKey(), + $this->foreignKey() + ); + if (Str::snake($relationName) === Str::snake($this->parent->getClassName())) { + $relationName = Str::plural($this->related->getClassName()); + } else { + $relationName = Str::plural($this->related->getClassName()) . 'Where' . ucfirst(Str::singular($relationName)); } - break; - case 'related': default: - $relationName = $relationBaseName; + case 'related': + $relationName = Str::plural($this->related->getClassName()); break; } @@ -67,4 +58,12 @@ public function method() { return 'hasMany'; } + + /** + * @return string + */ + public function returnType() + { + return \Illuminate\Database\Eloquent\Relations\HasMany::class; + } } diff --git a/src/Coders/Model/Relations/HasOne.php b/src/Coders/Model/Relations/HasOne.php index f0ef1cda..3381a853 100644 --- a/src/Coders/Model/Relations/HasOne.php +++ b/src/Coders/Model/Relations/HasOne.php @@ -38,4 +38,12 @@ public function method() { return 'hasOne'; } + + /** + * @return string + */ + public function returnType() + { + return \Illuminate\Database\Eloquent\Relations\HasOne::class; + } } diff --git a/src/Coders/Model/Relations/HasOneOrManyStrategy.php b/src/Coders/Model/Relations/HasOneOrManyStrategy.php index 04b514d0..cb5d1fa3 100644 --- a/src/Coders/Model/Relations/HasOneOrManyStrategy.php +++ b/src/Coders/Model/Relations/HasOneOrManyStrategy.php @@ -60,4 +60,14 @@ public function body() { return $this->relation->body(); } + + /** + * @return string + */ + public function returnType() + { + return get_class($this->relation) === HasMany::class ? + \Illuminate\Database\Eloquent\Relations\HasMany::class : + \Illuminate\Database\Eloquent\Relations\HasOne::class; + } } diff --git a/src/Coders/Model/Relations/ReferenceFactory.php b/src/Coders/Model/Relations/ReferenceFactory.php index e0728245..6242fab2 100644 --- a/src/Coders/Model/Relations/ReferenceFactory.php +++ b/src/Coders/Model/Relations/ReferenceFactory.php @@ -73,7 +73,7 @@ protected function hasPivot() return false; } - $pivot = str_replace($firstRecord, '', $pivot); + $pivot = preg_replace("!$firstRecord!", '', $pivot, 1); foreach ($this->getRelatedBlueprint()->relations() as $reference) { if ($reference == $this->getRelatedReference()) { diff --git a/src/Coders/Model/Relations/RelationHelper.php b/src/Coders/Model/Relations/RelationHelper.php new file mode 100644 index 00000000..08006d7d --- /dev/null +++ b/src/Coders/Model/Relations/RelationHelper.php @@ -0,0 +1,30 @@ +created_by = $this->doer(); } /** - * @param string $event * @param \Illuminate\Database\Eloquent\Model $model */ - public function updating($event, Eloquent $model) + public function updating(Eloquent $model) { - $model->udpated_by = $this->doer(); + $model->updated_by = $this->doer(); } /** diff --git a/src/Meta/MySql/Column.php b/src/Meta/MySql/Column.php index dc4f9c57..e90ba717 100644 --- a/src/Meta/MySql/Column.php +++ b/src/Meta/MySql/Column.php @@ -29,11 +29,11 @@ class Column implements \Reliese\Meta\Column * @var array */ public static $mappings = [ - 'string' => ['varchar', 'text', 'string', 'char', 'enum', 'tinytext', 'mediumtext', 'longtext'], - 'date' => ['datetime', 'year', 'date', 'time', 'timestamp'], + 'string' => ['varchar', 'text', 'string', 'char', 'enum', 'set', 'tinytext', 'mediumtext', 'longtext', 'longblob', 'mediumblob', 'tinyblob', 'blob'], + 'datetime' => ['datetime', 'year', 'date', 'time', 'timestamp'], 'int' => ['bigint', 'int', 'integer', 'tinyint', 'smallint', 'mediumint'], 'float' => ['float', 'decimal', 'numeric', 'dec', 'fixed', 'double', 'real', 'double precision'], - 'boolean' => ['longblob', 'blob', 'bit'], + 'boolean' => ['bit'] ]; /** diff --git a/src/Meta/MySql/Schema.php b/src/Meta/MySql/Schema.php index 186010f7..d7256dd6 100644 --- a/src/Meta/MySql/Schema.php +++ b/src/Meta/MySql/Schema.php @@ -270,8 +270,8 @@ protected function resolveForeignTable($table, Blueprint $blueprint) */ public static function schemas(Connection $connection) { - $schemas = $connection->getDoctrineSchemaManager()->listDatabases(); - + $schemas = $connection->select('SELECT schema_name FROM information_schema.schemata'); + $schemas = array_column($schemas, 'schema_name'); return array_diff($schemas, [ 'information_schema', 'sys', diff --git a/src/Meta/Postgres/Column.php b/src/Meta/Postgres/Column.php index c80b0bef..c5a2b604 100644 --- a/src/Meta/Postgres/Column.php +++ b/src/Meta/Postgres/Column.php @@ -20,7 +20,7 @@ class Column implements \Reliese\Meta\Column * @var array */ protected $metas = [ - 'type', 'name', 'autoincrement', 'nullable', 'default', 'comment', + 'type', 'name', 'autoincrement', 'nullable', 'default', 'comment', ]; /** @@ -28,8 +28,8 @@ class Column implements \Reliese\Meta\Column * @todo check these */ public static $mappings = [ - 'string' => ['varchar', 'text', 'string', 'char', 'enum', 'tinytext', 'mediumtext', 'longtext', 'json'], - 'date' => ['datetime', 'year', 'date', 'time', 'timestamp'], + 'string' => ['character varying', 'varchar', 'text', 'string', 'char', 'character', 'enum', 'tinytext', 'mediumtext', 'longtext', 'json'], + 'datetime' => ['timestamp with time zone', 'timestamp without time zone', 'timestamptz', 'datetime', 'year', 'date', 'time', 'timestamp'], 'int' => ['int', 'integer', 'tinyint', 'smallint', 'mediumint', 'bigint', 'bigserial', 'serial', 'smallserial', 'tinyserial', 'serial4', 'serial8'], 'float' => ['float', 'decimal', 'numeric', 'dec', 'fixed', 'double', 'real', 'double precision'], 'boolean' => ['boolean', 'bool', 'bit'], @@ -84,13 +84,12 @@ protected function parseType(Fluent $attributes) */ protected function parsePrecision($databaseType, Fluent $attributes) { - $precision = $this->get('numeric_precision', 'string'); - $precision = explode(',', str_replace("'", '', $precision)); + $precision = $this->get('numeric_precision', ''); // Default to empty string instead of 'string' + $precision = explode(',', str_replace("'", '', $precision ?? '')); // Ensure $precision is string // Check whether it's an enum if ($databaseType == 'enum') { //$attributes['enum'] = $precision; //todo - return; } @@ -194,7 +193,9 @@ protected function same($key, $value) private function defaultIsNextVal(Fluent $attributes) { $value = $this->get('column_default', $this->get('generation_expression', null)); + $isIdentity = $this->get('is_identity'); + $identityGeneration = $this->get('identity_generation'); - return preg_match('/nextval\(/i', $value); + return preg_match('/nextval\(/i', $value ?? '') || ($isIdentity === 'YES' && $identityGeneration === 'BY DEFAULT'); } } diff --git a/src/Meta/Postgres/Schema.php b/src/Meta/Postgres/Schema.php index abe2c455..be986c69 100644 --- a/src/Meta/Postgres/Schema.php +++ b/src/Meta/Postgres/Schema.php @@ -3,6 +3,7 @@ namespace Reliese\Meta\Postgres; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Config; use Reliese\Meta\Blueprint; use Illuminate\Support\Fluent; use Illuminate\Database\Connection; @@ -33,6 +34,11 @@ class Schema implements \Reliese\Meta\Schema */ protected $tables = []; + /** + * @var mixed|null + */ + protected $schema_database = null; + /** * Mapper constructor. * @@ -41,6 +47,10 @@ class Schema implements \Reliese\Meta\Schema */ public function __construct($schema, $connection) { + $this->schema_database = Config::get("database.connections.pgsql.schema"); + if (!$this->schema_database){ + $this->schema_database = 'public'; + } $this->schema = $schema; $this->connection = $connection; @@ -82,7 +92,7 @@ protected function load() protected function fetchTables() { $rows = $this->arraify($this->connection->select( - 'SELECT * FROM pg_tables where schemaname=\'public\'' + "SELECT * FROM pg_tables where schemaname='$this->schema_database'" )); $names = array_column($rows, 'tablename'); @@ -96,7 +106,7 @@ protected function fillColumns(Blueprint $blueprint) { $rows = $this->arraify($this->connection->select( 'SELECT * FROM information_schema.columns '. - 'WHERE table_schema=\'public\''. + "WHERE table_schema='$this->schema_database'". 'AND table_name='.$this->wrap($blueprint->table()) )); foreach ($rows as $column) { @@ -273,7 +283,8 @@ protected function wrap($table) */ public static function schemas(Connection $connection) { - $schemas = $connection->getDoctrineSchemaManager()->listDatabases(); + $schemas = $connection->select('SELECT datname FROM pg_database'); + $schemas = array_column($schemas, 'datname'); return array_diff($schemas, [ 'postgres', diff --git a/src/Meta/SchemaManager.php b/src/Meta/SchemaManager.php index ba98da05..89d37505 100644 --- a/src/Meta/SchemaManager.php +++ b/src/Meta/SchemaManager.php @@ -10,6 +10,7 @@ use ArrayIterator; use RuntimeException; use IteratorAggregate; +use Traversable; // Added this line use Illuminate\Database\MySqlConnection; use Illuminate\Database\SQLiteConnection; use Illuminate\Database\PostgresConnection; @@ -28,6 +29,7 @@ class SchemaManager implements IteratorAggregate SQLiteConnection::class => SqliteSchema::class, PostgresConnection::class => PostgresSchema::class, \Larapack\DoctrineSupport\Connections\MySqlConnection::class => MySqlSchema::class, + \Staudenmeir\LaravelCte\Connections\MySqlConnection::class => MySqlSchema::class, ]; /** @@ -134,7 +136,7 @@ public static function register($connection, $mapper) * * @return \ArrayIterator */ - public function getIterator() + public function getIterator(): Traversable { return new ArrayIterator($this->schemas); } diff --git a/src/Meta/Sqlite/Column.php b/src/Meta/Sqlite/Column.php index b7a263ab..068d4ab7 100644 --- a/src/Meta/Sqlite/Column.php +++ b/src/Meta/Sqlite/Column.php @@ -28,7 +28,7 @@ class Column implements \Reliese\Meta\Column */ public static $mappings = [ 'string' => ['varchar', 'text', 'string', 'char', 'enum', 'tinytext', 'mediumtext', 'longtext'], - 'date' => ['datetime', 'year', 'date', 'time', 'timestamp'], + 'datetime' => ['datetime', 'year', 'date', 'time', 'timestamp'], 'int' => ['bigint', 'int', 'integer', 'tinyint', 'smallint', 'mediumint'], 'float' => ['float', 'decimal', 'numeric', 'dec', 'fixed', 'double', 'real', 'double precision'], 'boolean' => ['longblob', 'blob', 'bit'], diff --git a/src/Meta/Sqlite/Schema.php b/src/Meta/Sqlite/Schema.php index 7c611692..54a2a5c6 100644 --- a/src/Meta/Sqlite/Schema.php +++ b/src/Meta/Sqlite/Schema.php @@ -144,7 +144,7 @@ protected function fillPrimaryKey(Blueprint $blueprint) $key = [ 'name' => 'primary', 'index' => '', - 'columns' => $indexes['primary']->getColumns(), + 'columns' => optional($indexes['primary']??null)->getColumns()?:[], ]; $blueprint->withPrimaryKey(new Fluent($key)); diff --git a/src/Support/Classify.php b/src/Support/Classify.php index 1533d6ba..57b40052 100644 --- a/src/Support/Classify.php +++ b/src/Support/Classify.php @@ -67,8 +67,10 @@ public function field($name, $value, $options = []) public function method($name, $body, $options = []) { $visibility = Arr::get($options, 'visibility', 'public'); + $returnType = Arr::get($options, 'returnType', null); + $formattedReturnType = $returnType ? ': '.$returnType : ''; - return "\n\t$visibility function $name()\n\t{\n\t\t$body\n\t}\n"; + return "\n\t$visibility function $name()$formattedReturnType\n\t{\n\t\t$body\n\t}\n"; } public function mixin($class) diff --git a/tests/Coders/Model/Relations/BelongsToTest.php b/tests/Coders/Model/Relations/BelongsToTest.php new file mode 100644 index 00000000..4c2536fa --- /dev/null +++ b/tests/Coders/Model/Relations/BelongsToTest.php @@ -0,0 +1,94 @@ +makePartial(); + + $relatedModel = Mockery::mock(Model::class)->makePartial(); + + $subject = Mockery::mock(Model::class)->makePartial(); + $subject->shouldReceive('getRelationNameStrategy')->andReturn('foreign_key'); + $subject->shouldReceive('usesSnakeAttributes')->andReturn($usesSnakeAttributes); + + /** @var BelongsTo|\Mockery\Mock $relationship */ + $relationship = Mockery::mock(BelongsTo::class, [$relation, $subject, $relatedModel])->makePartial(); + $relationship->shouldAllowMockingProtectedMethods(); + $relationship->shouldReceive('otherKey')->andReturn($primaryKey); + $relationship->shouldReceive('foreignKey')->andReturn($foreignKey); + + $this->assertEquals( + $expected, + $relationship->name(), + json_encode(compact('usesSnakeAttributes', 'primaryKey', 'foreignKey')) + ); + } + + public function provideRelatedStrategyPermutations() + { + // usesSnakeAttributes, relatedClassName, expected + return [ + [false, 'LineManager', 'lineManager'], + [true, 'LineManager', 'line_manager'], + ]; + } + + /** + * @dataProvider provideRelatedStrategyPermutations + * + * @param bool $usesSnakeAttributes + * @param string $relatedClassName + * @param string $expected + */ + public function testNameUsingRelatedStrategy($usesSnakeAttributes, $relatedClassName, $expected) + { + $relation = Mockery::mock(Fluent::class)->makePartial(); + + $relatedModel = Mockery::mock(Model::class)->makePartial(); + $relatedModel->shouldReceive('getClassName')->andReturn($relatedClassName); + + $subject = Mockery::mock(Model::class)->makePartial(); + $subject->shouldReceive('getRelationNameStrategy')->andReturn('related'); + $subject->shouldReceive('usesSnakeAttributes')->andReturn($usesSnakeAttributes); + + /** @var BelongsTo|\Mockery\Mock $relationship */ + $relationship = Mockery::mock(BelongsTo::class, [$relation, $subject, $relatedModel])->makePartial(); + + $this->assertEquals( + $expected, + $relationship->name(), + json_encode(compact('usesSnakeAttributes', 'relatedClassName')) + ); + } +} diff --git a/tests/Coders/Model/Relations/HasManyTest.php b/tests/Coders/Model/Relations/HasManyTest.php new file mode 100644 index 00000000..e9e79d80 --- /dev/null +++ b/tests/Coders/Model/Relations/HasManyTest.php @@ -0,0 +1,111 @@ +makePartial(); + + $relatedModel = Mockery::mock(Model::class)->makePartial(); + $relatedModel->shouldReceive('getClassName')->andReturn($relationName); + + $subject = Mockery::mock(Model::class)->makePartial(); + $subject->shouldReceive('getRelationNameStrategy')->andReturn('foreign_key'); + $subject->shouldReceive('usesSnakeAttributes')->andReturn($usesSnakeAttributes); + $subject->shouldReceive('getClassName')->andReturn($subjectName); + + /** @var BelongsTo|\Mockery\Mock $relationship */ + $relationship = Mockery::mock(HasMany::class, [$relation, $subject, $relatedModel])->makePartial(); + $relationship->shouldAllowMockingProtectedMethods(); + $relationship->shouldReceive('localKey')->andReturn($primaryKey); + $relationship->shouldReceive('foreignKey')->andReturn($foreignKey); + + $this->assertEquals( + $expected, + $relationship->name(), + json_encode(compact('usesSnakeAttributes', 'subjectName', 'relationName', 'primaryKey', 'foreignKey')) + ); + } + + public function provideRelatedStrategyPermutations() + { + // usesSnakeAttributes, subjectName, relatedName, expected + return [ + [false, 'StaffMember', 'BlogPost', 'blogPosts'], + [true, 'StaffMember', 'BlogPost', 'blog_posts'], + // Same table reference + [false, 'StaffMember', 'StaffMember', 'staffMembers'] + ]; + } + + /** + * @dataProvider provideRelatedStrategyPermutations + * + * @param bool $usesSnakeAttributes + * @param string $subjectName + * @param string $relationName + * @param string $expected + */ + public function testNameUsingRelatedStrategy($usesSnakeAttributes, $subjectName, $relationName, $expected) + { + $relation = Mockery::mock(Fluent::class)->makePartial(); + + $relatedModel = Mockery::mock(Model::class)->makePartial(); + $relatedModel->shouldReceive('getClassName')->andReturn($relationName); + + $subject = Mockery::mock(Model::class)->makePartial(); + $subject->shouldReceive('getClassName')->andReturn($subjectName); + $subject->shouldReceive('getRelationNameStrategy')->andReturn('related'); + $subject->shouldReceive('usesSnakeAttributes')->andReturn($usesSnakeAttributes); + + /** @var BelongsTo|\Mockery\Mock $relationship */ + $relationship = Mockery::mock(HasMany::class, [$relation, $subject, $relatedModel])->makePartial(); + + $this->assertEquals( + $expected, + $relationship->name(), + json_encode(compact('usesSnakeAttributes', 'subjectName', 'relationName')) + ); + } +} diff --git a/tests/Coders/Model/Relations/RelationHelperTest.php b/tests/Coders/Model/Relations/RelationHelperTest.php new file mode 100644 index 00000000..87c0ba0c --- /dev/null +++ b/tests/Coders/Model/Relations/RelationHelperTest.php @@ -0,0 +1,43 @@ +assertEquals( + $expected, + RelationHelper::stripSuffixFromForeignKey($usesSnakeAttributes, $primaryKey, $foreignKey), + json_encode(compact('usesSnakeAttributes', 'primaryKey', 'foreignKey')) + ); + } +}