├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENCE.md ├── README.md ├── composer.json ├── config └── models.php ├── phpunit.xml ├── src ├── Coders │ ├── CodersServiceProvider.php │ ├── Console │ │ └── CodeModelsCommand.php │ └── Model │ │ ├── Config.php │ │ ├── Factory.php │ │ ├── Model.php │ │ ├── ModelManager.php │ │ ├── Mutation.php │ │ ├── Mutator.php │ │ ├── Relation.php │ │ ├── Relations │ │ ├── BelongsTo.php │ │ ├── BelongsToMany.php │ │ ├── HasMany.php │ │ ├── HasOne.php │ │ ├── HasOneOrMany.php │ │ ├── HasOneOrManyStrategy.php │ │ ├── ReferenceFactory.php │ │ └── RelationHelper.php │ │ └── Templates │ │ ├── model │ │ └── user_model ├── Database │ └── Eloquent │ │ ├── BitBooleans.php │ │ ├── BlamableBehavior.php │ │ ├── Model.php │ │ └── WhoDidIt.php ├── Meta │ ├── Blueprint.php │ ├── Column.php │ ├── MySql │ │ ├── Column.php │ │ └── Schema.php │ ├── Postgres │ │ ├── Column.php │ │ └── Schema.php │ ├── Schema.php │ ├── SchemaManager.php │ └── Sqlite │ │ ├── Column.php │ │ └── Schema.php └── Support │ ├── Classify.php │ └── Dumper.php └── tests ├── Coders ├── Console │ └── Model │ │ └── ModelTest.php └── Model │ ├── ConfigTest.php │ └── Relations │ ├── BelongsToTest.php │ ├── HasManyTest.php │ └── RelationHelperTest.php ├── Meta └── BlueprintTest.php ├── TestCase.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor 3 | composer.lock 4 | .phpunit.result.cache -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '7.3' 5 | - '7.4' 6 | 7 | sudo: false 8 | 9 | cache: 10 | directories: 11 | - $HOME/.composer/cache 12 | 13 | install: 14 | - travis_retry composer install 15 | 16 | script: vendor/bin/phpunit -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## v0.0.13 (2017-02-04) 4 | 5 | ### Fixed 6 | - Error when using setFetchMode on Laravel Connection [#13](https://github.com/reliese/laravel/issues/11) 7 | 8 | ## v0.0.12 (2017-01-08) 9 | 10 | ### Fixed 11 | - Error when table with relationships does not have a primary key [#11](https://github.com/reliese/laravel/issues/11) 12 | 13 | ## v0.0.11 (2016-11-01) 14 | 15 | ### Added 16 | - CHANGELOG and LICENSE files 17 | 18 | ### Fixed 19 | - Only retrieve details for tables and not views ([#6](https://github.com/reliese/laravel/pull/6)) 20 | 21 | 22 | ## v0.0.10 (2016-11-01) 23 | 24 | ### Fixed 25 | - Add missing backticks to database queries ([#5](https://github.com/reliese/laravel/issues/5), [#2](https://github.com/reliese/laravel/issues/2)) 26 | 27 | 28 | ## v0.0.9 (2016-10-23) 29 | 30 | ### Changed 31 | - Treat Soft Deletes field as string 32 | 33 | 34 | ## v0.0.8 (2016-10-23) 35 | 36 | ### Fixed 37 | - Table name plural form resolution ([#3](https://github.com/reliese/laravel/issues/3)) 38 | 39 | 40 | ## v0.0.7 (2016-10-22) 41 | 42 | ### Added 43 | - Travis file and badge 44 | 45 | ### Changed 46 | - Extended Laravel compatibility from 5.1 above. 47 | 48 | 49 | ## v0.0.6 (2016-10-20) 50 | 51 | ### Added 52 | - StyleCI badge 53 | 54 | ### Changed 55 | - Updated composer dependencies 56 | 57 | 58 | ## v0.0.5 (2016-10-16) 59 | 60 | ### Changed 61 | - Stop casting Soft Deletes field as date 62 | 63 | 64 | ## v0.0.4 (2016-10-16) 65 | 66 | ### Changed 67 | - Changed Package name in comment 68 | 69 | 70 | ## v0.0.3 (2016-10-16) 71 | 72 | ### Changed 73 | - Optional model hints 74 | 75 | 76 | ## v0.0.2 (2016-10-16) 77 | 78 | ### Changed 79 | - Updated description 80 | 81 | ## v0.0.1 (2016-10-16) 82 | 83 | ### Added 84 | - Created Project 85 | 86 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Cristian Llanos 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reliese Laravel Model Generator 2 | [![Build Status](https://travis-ci.org/reliese/laravel.svg?branch=master)](https://travis-ci.org/reliese/laravel) 3 | [![Latest Stable Version](https://poser.pugx.org/reliese/laravel/v/stable)](https://packagist.org/packages/reliese/laravel) 4 | [![Total Downloads](https://poser.pugx.org/reliese/laravel/downloads)](https://packagist.org/packages/reliese/laravel) 5 | [![Latest Unstable Version](https://poser.pugx.org/reliese/laravel/v/unstable)](https://packagist.org/packages/reliese/laravel) 6 | [![License](https://poser.pugx.org/reliese/laravel/license)](https://packagist.org/packages/reliese/laravel) 7 | 8 | Reliese Laravel Model Generator aims to speed up the development process of Laravel applications by 9 | providing some convenient code-generation capabilities. 10 | The tool inspects your database structure, including column names and foreign keys, in order 11 | to automatically generate Models that have correctly typed properties, along with any relationships to other Models. 12 | 13 | ## How does it work? 14 | 15 | This package expects that you are using Laravel 5.1 or above. 16 | You will need to import the `reliese/laravel` package via composer: 17 | 18 | ### Configuration 19 | 20 | It is recommended that this package should only be used on a local environment for security reasons. You should install it via composer using the --dev option like this: 21 | 22 | ```shell 23 | composer require reliese/laravel --dev 24 | ``` 25 | 26 | Add the `models.php` configuration file to your `config` directory and clear the config cache: 27 | 28 | ```shell 29 | php artisan vendor:publish --tag=reliese-models 30 | 31 | # Let's refresh our config cache just in case 32 | php artisan config:clear 33 | ``` 34 | 35 | ## Models 36 | 37 | ![Generating models with artisan](https://cdn-images-1.medium.com/max/800/1*hOa2QxORE2zyO_-ZqJ40sA.png "Making artisan code my Eloquent models") 38 | 39 | ### Usage 40 | 41 | Assuming you have already configured your database, you are now all set to go. 42 | 43 | - Let's scaffold some of your models from your default connection. 44 | 45 | ```shell 46 | php artisan code:models 47 | ``` 48 | 49 | - You can scaffold a specific table like this: 50 | 51 | ```shell 52 | php artisan code:models --table=users 53 | ``` 54 | 55 | - You can also specify the connection: 56 | 57 | ```shell 58 | php artisan code:models --connection=mysql 59 | ``` 60 | 61 | - If you are using a MySQL database, you can specify which schema you want to scaffold: 62 | 63 | ```shell 64 | php artisan code:models --schema=shop 65 | ``` 66 | 67 | ### Customizing Model Scaffolding 68 | 69 | To change the scaffolding behaviour you can make `config/models.php` configuration file 70 | fit your database needs. [Check it out](https://github.com/reliese/laravel/blob/master/config/models.php) ;-) 71 | 72 | ### Tips 73 | 74 | #### 1. Keeping model changes 75 | 76 | You may want to generate your models as often as you change your database. In order 77 | not to lose your own model changes, you should set `base_files` to `true` in your `config/models.php`. 78 | 79 | When you enable this feature your models will inherit their base configurations from 80 | base models. You should avoid adding code to your base models, since you 81 | will lose all changes when they are generated again. 82 | 83 | > Note: You will end up with two models for the same table and you may think it is a horrible idea 84 | to have two classes for the same thing. However, it is up to you 85 | to decide whether this approach gives value to your project :-) 86 | 87 | #### Support 88 | 89 | For the time being, this package supports MySQL, PostgreSQL and SQLite databases. Support for other databases are encouraged to be added through pull requests. 90 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reliese/laravel", 3 | "description": "Reliese Components for Laravel Framework code generation.", 4 | "keywords": ["reliese", "laravel"], 5 | "homepage": "http://cristianllanos.com", 6 | "support": { 7 | "issues": "https://github.com/reliese/laravel/issues", 8 | "source": "https://github.com/reliese/laravel" 9 | }, 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Cristian Llanos", 14 | "email": "cristianllanos@outlook.com" 15 | } 16 | ], 17 | "require": { 18 | "php": "^7.3|^8.0", 19 | "doctrine/dbal": ">=2.5", 20 | "illuminate/support": ">=5.1", 21 | "illuminate/database": ">=5.1", 22 | "illuminate/contracts": ">=5.1", 23 | "illuminate/filesystem": ">=5.1", 24 | "illuminate/console": ">=5.1" 25 | }, 26 | "require-dev": { 27 | "fzaninotto/faker": "~1.4", 28 | "mockery/mockery": ">=1.4", 29 | "phpunit/phpunit": "^9" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Reliese\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "classmap": [ 38 | "tests/TestCase.php" 39 | ] 40 | }, 41 | "config": { 42 | "preferred-install": "dist" 43 | }, 44 | "extra": { 45 | "laravel": { 46 | "providers": [ 47 | "Reliese\\Coders\\CodersServiceProvider" 48 | ] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /config/models.php: -------------------------------------------------------------------------------- 1 | [ 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Model Files Location 20 | |-------------------------------------------------------------------------- 21 | | 22 | | We need a location to store your new generated files. All files will be 23 | | placed within this directory. When you turn on base files, they will 24 | | be placed within a Base directory inside this location. 25 | | 26 | */ 27 | 28 | 'path' => app_path('Models'), 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Model Namespace 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Every generated model will belong to this namespace. It is suggested 36 | | that this namespace should follow PSR-4 convention and be very 37 | | similar to the path of your models defined above. 38 | | 39 | */ 40 | 41 | 'namespace' => 'App\Models', 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Parent Class 46 | |-------------------------------------------------------------------------- 47 | | 48 | | All Eloquent models should inherit from Eloquent Model class. However, 49 | | you can define a custom Eloquent model that suits your needs. 50 | | As an example one custom model has been added for you which 51 | | will allow you to create custom database castings. 52 | | 53 | */ 54 | 55 | 'parent' => Illuminate\Database\Eloquent\Model::class, 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Traits 60 | |-------------------------------------------------------------------------- 61 | | 62 | | Sometimes you may want to append certain traits to all your models. 63 | | If that is what you need, you may list them bellow. 64 | | As an example we have a BitBooleans trait which will treat MySQL bit 65 | | data type as booleans. You might probably not need it, but it is 66 | | an example of how you can customize your models. 67 | | 68 | */ 69 | 70 | 'use' => [ 71 | // Reliese\Database\Eloquent\BitBooleans::class, 72 | // Reliese\Database\Eloquent\BlamableBehavior::class, 73 | ], 74 | 75 | /* 76 | |-------------------------------------------------------------------------- 77 | | Model Connection 78 | |-------------------------------------------------------------------------- 79 | | 80 | | If you wish your models had appended the connection from which they 81 | | were generated, you should set this value to true and your 82 | | models will have the connection property filled. 83 | | 84 | */ 85 | 86 | 'connection' => false, 87 | 88 | /* 89 | |-------------------------------------------------------------------------- 90 | | Timestamps 91 | |-------------------------------------------------------------------------- 92 | | 93 | | If your tables have CREATED_AT and UPDATED_AT timestamps you may 94 | | enable them and your models will fill their values as needed. 95 | | You can also specify which fields should be treated as timestamps 96 | | in case you don't follow the naming convention Eloquent uses. 97 | | If your table doesn't have these fields, timestamps will be 98 | | disabled for your model. 99 | | 100 | */ 101 | 102 | 'timestamps' => true, 103 | 104 | // 'timestamps' => [ 105 | // 'enabled' => true, 106 | // 'fields' => [ 107 | // 'CREATED_AT' => 'created_at', 108 | // 'UPDATED_AT' => 'updated_at', 109 | // ] 110 | // ], 111 | 112 | /* 113 | |-------------------------------------------------------------------------- 114 | | Soft Deletes 115 | |-------------------------------------------------------------------------- 116 | | 117 | | If your tables support soft deletes with a DELETED_AT attribute, 118 | | you can enable them here. You can also specify which field 119 | | should be treated as a soft delete attribute in case you 120 | | don't follow the naming convention Eloquent uses. 121 | | If your table doesn't have this field, soft deletes will be 122 | | disabled for your model. 123 | | 124 | */ 125 | 126 | 'soft_deletes' => true, 127 | 128 | // 'soft_deletes' => [ 129 | // 'enabled' => true, 130 | // 'field' => 'deleted_at', 131 | // ], 132 | 133 | /* 134 | |-------------------------------------------------------------------------- 135 | | Date Format 136 | |-------------------------------------------------------------------------- 137 | | 138 | | Here you may define your models' date format. The following format 139 | | is the default format Eloquent uses. You won't see it in your 140 | | models unless you change it to a more convenient value. 141 | | 142 | */ 143 | 144 | 'date_format' => 'Y-m-d H:i:s', 145 | 146 | /* 147 | |-------------------------------------------------------------------------- 148 | | Pagination 149 | |-------------------------------------------------------------------------- 150 | | 151 | | Here you may define how many models Eloquent should display when 152 | | paginating them. The default number is 15, so you might not 153 | | see this number in your models unless you change it. 154 | | 155 | */ 156 | 157 | 'per_page' => 15, 158 | 159 | /* 160 | |-------------------------------------------------------------------------- 161 | | Base Files 162 | |-------------------------------------------------------------------------- 163 | | 164 | | By default, your models will be generated in your models path, but 165 | | when you generate them again they will be replaced by new ones. 166 | | You may want to customize your models and, at the same time, be 167 | | able to generate them as your tables change. For that, you 168 | | can enable base files. These files will be replaced whenever 169 | | you generate them, but your customized files will not be touched. 170 | | 171 | */ 172 | 173 | 'base_files' => false, 174 | 175 | /* 176 | |-------------------------------------------------------------------------- 177 | | Snake Attributes 178 | |-------------------------------------------------------------------------- 179 | | 180 | | Eloquent treats your model attributes as snake cased attributes, but 181 | | if you have camel-cased fields in your database you can disable 182 | | that behaviour and use camel case attributes in your models. 183 | | 184 | */ 185 | 186 | 'snake_attributes' => true, 187 | 188 | /* 189 | |-------------------------------------------------------------------------- 190 | | Indent options 191 | |-------------------------------------------------------------------------- 192 | | 193 | | As default indention is done with tabs, but you can change it by setting 194 | | this to the amount of spaces you that you want to use for indentation. 195 | | Usually you will use 4 spaces instead of tabs. 196 | | 197 | */ 198 | 199 | 'indent_with_space' => 0, 200 | 201 | /* 202 | |-------------------------------------------------------------------------- 203 | | Qualified Table Names 204 | |-------------------------------------------------------------------------- 205 | | 206 | | If some of your tables have cross-database relationships (probably in 207 | | MySQL), you can make sure your models take into account their 208 | | respective database schema. 209 | | 210 | | Can Either be NULL, FALSE or TRUE 211 | | TRUE: Schema name will be prepended on the table 212 | | FALSE:Table name will be set without schema name. 213 | | NULL: Table name will follow laravel pattern, 214 | | i.e. if class name(plural) matches table name, then table name will not be added 215 | */ 216 | 217 | 'qualified_tables' => false, 218 | 219 | /* 220 | |-------------------------------------------------------------------------- 221 | | Hidden Attributes 222 | |-------------------------------------------------------------------------- 223 | | 224 | | When casting your models into arrays or json, the need to hide some 225 | | attributes sometimes arise. If your tables have some fields you 226 | | want to hide, you can define them bellow. 227 | | Some fields were defined for you. 228 | | 229 | */ 230 | 231 | 'hidden' => [ 232 | '*secret*', '*password', '*token', 233 | ], 234 | 235 | /* 236 | |-------------------------------------------------------------------------- 237 | | Mass Assignment Guarded Attributes 238 | |-------------------------------------------------------------------------- 239 | | 240 | | You may want to protect some fields from mass assignment. You can 241 | | define them bellow. Some fields were defined for you. 242 | | Your fillable attributes will be those which are not in the list 243 | | excluding your models' primary keys. 244 | | 245 | */ 246 | 247 | 'guarded' => [ 248 | // 'created_by', 'updated_by' 249 | ], 250 | 251 | /* 252 | |-------------------------------------------------------------------------- 253 | | Casts 254 | |-------------------------------------------------------------------------- 255 | | 256 | | You may want to specify which of your table fields should be cast as 257 | | something other than a string. For instance, you may want a 258 | | text field be cast as an array or and object. 259 | | 260 | | You may define column patterns which will be cast using the value 261 | | assigned. We have defined some fields for you. Feel free to 262 | | modify them to fit your needs. 263 | | 264 | */ 265 | 266 | 'casts' => [ 267 | '*_json' => 'json', 268 | ], 269 | 270 | /* 271 | |-------------------------------------------------------------------------- 272 | | Excluded Tables 273 | |-------------------------------------------------------------------------- 274 | | 275 | | When performing the generation of models you may want to skip some of 276 | | them, because you don't want a model for them or any other reason. 277 | | You can define those tables bellow. The migrations table was 278 | | filled for you, since you may not want a model for it. 279 | | 280 | */ 281 | 282 | 'except' => [ 283 | 'migrations', 284 | 'failed_jobs', 285 | 'password_resets', 286 | 'personal_access_tokens', 287 | 'password_reset_tokens', 288 | ], 289 | 290 | /* 291 | |-------------------------------------------------------------------------- 292 | | Specified Tables 293 | |-------------------------------------------------------------------------- 294 | | 295 | | You can specify specific tables. This will generate the models only 296 | | for selected tables, ignoring the rest. 297 | | 298 | */ 299 | 300 | 'only' => [ 301 | // 'users', 302 | ], 303 | 304 | /* 305 | |-------------------------------------------------------------------------- 306 | | Table Prefix 307 | |-------------------------------------------------------------------------- 308 | | 309 | | If you have a prefix on your table names but don't want it in the model 310 | | and relation names, specify it here. 311 | | 312 | */ 313 | 314 | 'table_prefix' => '', 315 | 316 | /* 317 | |-------------------------------------------------------------------------- 318 | | Lower table name before doing studly 319 | |-------------------------------------------------------------------------- 320 | | 321 | | If tables names are capitalised using studly produces incorrect name 322 | | this can help fix it ie TABLE_NAME now becomes TableName 323 | | 324 | */ 325 | 326 | 'lower_table_name_first' => false, 327 | 328 | /* 329 | |-------------------------------------------------------------------------- 330 | | Model Names 331 | |-------------------------------------------------------------------------- 332 | | 333 | | By default the generator will create models with names that match your tables. 334 | | However, if you wish to manually override the naming, you can specify a mapping 335 | | here between table and model names. 336 | | 337 | | Example: 338 | | A table called 'billing_invoices' will generate a model called `BillingInvoice`, 339 | | but you'd prefer it to generate a model called 'Invoice'. Therefore, you'd add 340 | | the following array key and value: 341 | | 'billing_invoices' => 'Invoice', 342 | */ 343 | 344 | 'model_names' => [ 345 | 346 | ], 347 | 348 | /* 349 | |-------------------------------------------------------------------------- 350 | | Relation Name Strategy 351 | |-------------------------------------------------------------------------- 352 | | 353 | | How the relations should be named in your models. 354 | | 355 | | 'related' Use the related table as the relation name. 356 | | (post.author --> user.id) 357 | generates Post::user() and User::posts() 358 | | 359 | | 'foreign_key' Use the foreign key as the relation name. 360 | | This can help to provide more meaningful relationship names, and avoids naming conflicts 361 | | if you have more than one relationship between two tables. 362 | | (post.author_id --> user.id) 363 | | generates Post::author() and User::posts_where_author() 364 | | (post.editor_id --> user.id) 365 | | generates Post::editor() and User::posts_where_editor() 366 | | ID suffixes can be omitted from foreign keys. 367 | | (post.author --> user.id) 368 | | (post.editor --> user.id) 369 | | generates the same as above. 370 | | Where the foreign key matches the related table name, it behaves as per the 'related' strategy. 371 | | (post.user_id --> user.id) 372 | | generates Post::user() and User::posts() 373 | */ 374 | 375 | 'relation_name_strategy' => 'related', 376 | // 'relation_name_strategy' => 'foreign_key', 377 | 378 | /* 379 | |-------------------------------------------------------------------------- 380 | | Determines need or not to generate constants with properties names like 381 | | 382 | | ... 383 | | const AGE = 'age'; 384 | | const USER_NAME = 'user_name'; 385 | | ... 386 | | 387 | | that later can be used in QueryBuilder like 388 | | 389 | | ... 390 | | $builder->select([User::USER_NAME])->where(User::AGE, '<=', 18); 391 | | ... 392 | | 393 | | that helps to avoid typos in strings when typing field names and allows to use 394 | | code competition with available model's field names. 395 | */ 396 | 'with_property_constants' => false, 397 | 398 | /* 399 | |-------------------------------------------------------------------------- 400 | | Optionally includes a full list of columns in the base generated models, 401 | | which can be used to avoid making calls like 402 | | 403 | | ... 404 | | \Illuminate\Support\Facades\Schema::getColumnListing 405 | | ... 406 | | 407 | | which can be slow, especially for large tables. 408 | */ 409 | 'with_column_list' => false, 410 | 411 | /* 412 | |-------------------------------------------------------------------------- 413 | | Disable Pluralization Name 414 | |-------------------------------------------------------------------------- 415 | | 416 | | You can disable pluralization tables and relations 417 | | 418 | */ 419 | 'pluralize' => true, 420 | 421 | /* 422 | |-------------------------------------------------------------------------- 423 | | Disable Pluralization Except For Certain Tables 424 | |-------------------------------------------------------------------------- 425 | | 426 | | You can enable pluralization for certain tables 427 | | 428 | */ 429 | 'override_pluralize_for' => [ 430 | 431 | ], 432 | 433 | /* 434 | |-------------------------------------------------------------------------- 435 | | Move $hidden property to base files 436 | |-------------------------------------------------------------------------- 437 | | When base_files is true you can set hidden_in_base_files to true 438 | | if you want the $hidden to be generated in base files 439 | | 440 | */ 441 | 'hidden_in_base_files' => false, 442 | 443 | /* 444 | |-------------------------------------------------------------------------- 445 | | Move $fillable property to base files 446 | |-------------------------------------------------------------------------- 447 | | When base_files is true you can set fillable_in_base_files to true 448 | | if you want the $fillable to be generated in base files 449 | | 450 | */ 451 | 'fillable_in_base_files' => false, 452 | 453 | /* 454 | |-------------------------------------------------------------------------- 455 | | Generate return types for relation methods. 456 | |-------------------------------------------------------------------------- 457 | | When enable_return_types is set to true, return type declarations are added 458 | | to all generated relation methods for your models. 459 | | 460 | | NOTE: This requires PHP 7.0 or later. 461 | | 462 | */ 463 | 'enable_return_types' => false, 464 | ], 465 | 466 | /* 467 | |-------------------------------------------------------------------------- 468 | | Database Specifics 469 | |-------------------------------------------------------------------------- 470 | | 471 | | In this section you may define the default configuration for each model 472 | | that will be generated from a specific database. You can also nest 473 | | table specific configurations. 474 | | These values will override those defined in the section above. 475 | | 476 | */ 477 | 478 | // 'shop' => [ 479 | // 'path' => app_path(), 480 | // 'namespace' => 'App', 481 | // 'snake_attributes' => false, 482 | // 'qualified_tables' => true, 483 | // 'use' => [ 484 | // Reliese\Database\Eloquent\BitBooleans::class, 485 | // ], 486 | // 'except' => ['migrations'], 487 | // 'only' => ['users'], 488 | // // Table Specifics Bellow: 489 | // 'user' => [ 490 | // // Don't use any default trait 491 | // 'use' => [], 492 | // ] 493 | // ], 494 | 495 | /* 496 | |-------------------------------------------------------------------------- 497 | | Connection Specifics 498 | |-------------------------------------------------------------------------- 499 | | 500 | | In this section you may define the default configuration for each model 501 | | that will be generated from a specific connection. You can also nest 502 | | database and table specific configurations. 503 | | 504 | | You may wish to use connection specific config for setting a parent 505 | | model with a read only setup, or enforcing a different set of rules 506 | | for a connection, e.g. using snake_case naming over CamelCase naming. 507 | | 508 | | This supports nesting with the following key configuration values, in 509 | | reverse precedence order (i.e. the last one found becomes the value). 510 | | 511 | | connections.{connection_name}.property 512 | | connections.{connection_name}.{database_name}.property 513 | | connections.{connection_name}.{table_name}.property 514 | | connections.{connection_name}.{database_name}.{table_name}.property 515 | | 516 | | These values will override those defined in the section above. 517 | | 518 | */ 519 | 520 | // 'connections' => [ 521 | // 'read_only_external' => [ 522 | // 'parent' => \App\Models\ReadOnlyModel::class, 523 | // 'connection' => true, 524 | // 'users' => [ 525 | // 'connection' => false, 526 | // ], 527 | // 'my_other_database' => [ 528 | // 'password_resets' => [ 529 | // 'connection' => false, 530 | // ] 531 | // ] 532 | // ], 533 | // ], 534 | ]; 535 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | 9 | 10 | ./tests 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Coders/CodersServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 27 | $this->publishes([ 28 | __DIR__.'/../../config/models.php' => config_path('models.php'), 29 | ], 'reliese-models'); 30 | 31 | $this->commands([ 32 | CodeModelsCommand::class, 33 | ]); 34 | } 35 | } 36 | 37 | /** 38 | * Register the application services. 39 | * 40 | * @return void 41 | */ 42 | public function register() 43 | { 44 | $this->registerModelFactory(); 45 | } 46 | 47 | /** 48 | * Register Model Factory. 49 | * 50 | * @return void 51 | */ 52 | protected function registerModelFactory() 53 | { 54 | $this->app->singleton(ModelFactory::class, function ($app) { 55 | return new ModelFactory( 56 | $app->make('db'), 57 | $app->make(Filesystem::class), 58 | new Classify(), 59 | new Config($app->make('config')->get('models')) 60 | ); 61 | }); 62 | } 63 | 64 | /** 65 | * @return array 66 | */ 67 | public function provides() 68 | { 69 | return [ModelFactory::class]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Coders/Console/CodeModelsCommand.php: -------------------------------------------------------------------------------- 1 | models = $models; 49 | $this->config = $config; 50 | } 51 | 52 | /** 53 | * Execute the console command. 54 | */ 55 | public function handle() 56 | { 57 | $connection = $this->getConnection(); 58 | $schema = $this->getSchema($connection); 59 | $table = $this->getTable(); 60 | 61 | // Check whether we just need to generate one table 62 | if ($table) { 63 | $this->models->on($connection)->create($schema, $table); 64 | $this->info("Check out your models for $table"); 65 | } 66 | 67 | // Otherwise map the whole database 68 | else { 69 | $this->models->on($connection)->map($schema); 70 | $this->info("Check out your models for $schema"); 71 | } 72 | } 73 | 74 | /** 75 | * @return string 76 | */ 77 | protected function getConnection() 78 | { 79 | return $this->option('connection') ?: $this->config->get('database.default'); 80 | } 81 | 82 | /** 83 | * @param $connection 84 | * 85 | * @return string 86 | */ 87 | protected function getSchema($connection) 88 | { 89 | return $this->option('schema') ?: $this->config->get("database.connections.$connection.database"); 90 | } 91 | 92 | /** 93 | * @return string 94 | */ 95 | protected function getTable() 96 | { 97 | return $this->option('table'); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Coders/Model/Config.php: -------------------------------------------------------------------------------- 1 | config = $config; 28 | } 29 | 30 | /** 31 | * @param \Reliese\Meta\Blueprint $blueprint 32 | * @param string $key 33 | * @param mixed $default 34 | * 35 | * @return mixed 36 | */ 37 | public function get(Blueprint $blueprint, $key, $default = null) 38 | { 39 | $priorityKeys = [ 40 | "@connections.{$blueprint->connection()}.{$blueprint->table()}.$key", 41 | "@connections.{$blueprint->connection()}.{$blueprint->schema()}.$key", 42 | "@connections.{$blueprint->connection()}.$key", 43 | "{$blueprint->qualifiedTable()}.$key", 44 | "{$blueprint->schema()}.$key", 45 | "*.$key", 46 | ]; 47 | 48 | foreach ($priorityKeys as $key) { 49 | $value = Arr::get($this->config, $key); 50 | 51 | if (!is_null($value)) { 52 | return $value; 53 | } 54 | } 55 | 56 | return $default; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Coders/Model/Factory.php: -------------------------------------------------------------------------------- 1 | db = $db; 65 | $this->files = $files; 66 | $this->config = $config; 67 | $this->class = $writer; 68 | } 69 | 70 | /** 71 | * @return \Reliese\Coders\Model\Mutator 72 | */ 73 | public function mutate() 74 | { 75 | return $this->mutators[] = new Mutator(); 76 | } 77 | 78 | /** 79 | * @return \Reliese\Coders\Model\ModelManager 80 | */ 81 | protected function models() 82 | { 83 | if (! isset($this->models)) { 84 | $this->models = new ModelManager($this); 85 | } 86 | 87 | return $this->models; 88 | } 89 | 90 | /** 91 | * Select connection to work with. 92 | * 93 | * @param string $connection 94 | * 95 | * @return $this 96 | */ 97 | public function on($connection = null) 98 | { 99 | $this->schemas = new SchemaManager($this->db->connection($connection)); 100 | 101 | return $this; 102 | } 103 | 104 | /** 105 | * @param string $schema 106 | */ 107 | public function map($schema) 108 | { 109 | if (! isset($this->schemas)) { 110 | $this->on(); 111 | } 112 | 113 | $mapper = $this->makeSchema($schema); 114 | 115 | foreach ($mapper->tables() as $blueprint) { 116 | if ($this->shouldTakeOnly($blueprint) && $this->shouldNotExclude($blueprint)) { 117 | $this->create($mapper->schema(), $blueprint->table()); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * @param \Reliese\Meta\Blueprint $blueprint 124 | * 125 | * @return bool 126 | */ 127 | protected function shouldNotExclude(Blueprint $blueprint) 128 | { 129 | foreach ($this->config($blueprint, 'except', []) as $pattern) { 130 | if (Str::is($pattern, $blueprint->table())) { 131 | return false; 132 | } 133 | } 134 | 135 | return true; 136 | } 137 | 138 | /** 139 | * @param \Reliese\Meta\Blueprint $blueprint 140 | * 141 | * @return bool 142 | */ 143 | protected function shouldTakeOnly(Blueprint $blueprint) 144 | { 145 | if ($patterns = $this->config($blueprint, 'only', [])) { 146 | foreach ($patterns as $pattern) { 147 | if (Str::is($pattern, $blueprint->table())) { 148 | return true; 149 | } 150 | } 151 | 152 | return false; 153 | } 154 | 155 | return true; 156 | } 157 | 158 | /** 159 | * @param string $schema 160 | * @param string $table 161 | */ 162 | public function create($schema, $table) 163 | { 164 | $model = $this->makeModel($schema, $table); 165 | $template = $this->prepareTemplate($model, 'model'); 166 | 167 | $file = $this->fillTemplate($template, $model); 168 | 169 | if ($model->indentWithSpace()) { 170 | $file = str_replace("\t", str_repeat(' ', $model->indentWithSpace()), $file); 171 | } 172 | 173 | $this->files->put($this->modelPath($model, $model->usesBaseFiles() ? ['Base'] : []), $file); 174 | 175 | if ($this->needsUserFile($model)) { 176 | $this->createUserFile($model); 177 | } 178 | } 179 | 180 | /** 181 | * @param string $schema 182 | * @param string $table 183 | * 184 | * @param bool $withRelations 185 | * 186 | * @return \Reliese\Coders\Model\Model 187 | */ 188 | public function makeModel($schema, $table, $withRelations = true) 189 | { 190 | return $this->models()->make($schema, $table, $this->mutators, $withRelations); 191 | } 192 | 193 | /** 194 | * @param string $schema 195 | * 196 | * @return \Reliese\Meta\Schema 197 | */ 198 | public function makeSchema($schema) 199 | { 200 | return $this->schemas->make($schema); 201 | } 202 | 203 | /** 204 | * @param \Reliese\Coders\Model\Model $model 205 | * 206 | * @todo: Delegate workload to SchemaManager and ModelManager 207 | * 208 | * @return array 209 | */ 210 | public function referencing(Model $model) 211 | { 212 | $references = []; 213 | 214 | // TODO: SchemaManager should do this 215 | foreach ($this->schemas as $schema) { 216 | $references = array_merge($references, $schema->referencing($model->getBlueprint())); 217 | } 218 | 219 | // TODO: ModelManager should do this 220 | foreach ($references as &$related) { 221 | $blueprint = $related['blueprint']; 222 | $related['model'] = $model->getBlueprint()->is($blueprint->schema(), $blueprint->table()) 223 | ? $model 224 | : $this->makeModel($blueprint->schema(), $blueprint->table(), false); 225 | } 226 | 227 | return $references; 228 | } 229 | 230 | /** 231 | * @param \Reliese\Coders\Model\Model $model 232 | * @param string $name 233 | * 234 | * @return string 235 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 236 | */ 237 | protected function prepareTemplate(Model $model, $name) 238 | { 239 | $defaultFile = $this->path([__DIR__, 'Templates', $name]); 240 | $file = $this->config($model->getBlueprint(), "*.template.$name", $defaultFile); 241 | 242 | return $this->files->get($file); 243 | } 244 | 245 | /** 246 | * @param string $template 247 | * @param \Reliese\Coders\Model\Model $model 248 | * 249 | * @return mixed 250 | */ 251 | protected function fillTemplate($template, Model $model) 252 | { 253 | $template = str_replace('{{namespace}}', $model->getBaseNamespace(), $template); 254 | $template = str_replace('{{class}}', $model->getClassName(), $template); 255 | 256 | $properties = $this->properties($model); 257 | $dependencies = $this->shortenAndExtractImportableDependencies($properties, $model); 258 | $template = str_replace('{{properties}}', $properties, $template); 259 | 260 | $parentClass = $model->getParentClass(); 261 | $dependencies = array_merge($dependencies, $this->shortenAndExtractImportableDependencies($parentClass, $model)); 262 | $template = str_replace('{{parent}}', $parentClass, $template); 263 | 264 | $body = $this->body($model); 265 | $dependencies = array_merge($dependencies, $this->shortenAndExtractImportableDependencies($body, $model)); 266 | $template = str_replace('{{body}}', $body, $template); 267 | 268 | $imports = $this->imports(array_keys($dependencies), $model); 269 | $template = str_replace('{{imports}}', $imports, $template); 270 | 271 | return $template; 272 | } 273 | 274 | /** 275 | * Returns imports section for model. 276 | * 277 | * @param array $dependencies Array of imported classes 278 | * @param Model $model 279 | * @return string 280 | */ 281 | private function imports($dependencies, Model $model) 282 | { 283 | $imports = []; 284 | foreach ($dependencies as $dependencyClass) { 285 | // Skip when the same class 286 | if (trim($dependencyClass, "\\") == trim($model->getQualifiedUserClassName(), "\\")) { 287 | continue; 288 | } 289 | 290 | // Do not import classes from same namespace 291 | $inCurrentNamespacePattern = str_replace('\\', '\\\\', "/{$model->getBaseNamespace()}\\[a-zA-Z0-9_]*/"); 292 | if (preg_match($inCurrentNamespacePattern, $dependencyClass)) { 293 | continue; 294 | } 295 | 296 | $imports[] = "use {$dependencyClass};"; 297 | } 298 | 299 | sort($imports); 300 | 301 | return implode("\n", $imports); 302 | } 303 | 304 | /** 305 | * Extract and replace fully-qualified class names from placeholder. 306 | * 307 | * @param string $placeholder Placeholder to extract class names from. Rewrites value to content without FQN 308 | * @param \Reliese\Coders\Model\Model $model 309 | * 310 | * @return array Extracted FQN 311 | */ 312 | private function shortenAndExtractImportableDependencies(&$placeholder, $model) 313 | { 314 | $qualifiedClassesPattern = '/([\\\\a-zA-Z0-9_]*\\\\[\\\\a-zA-Z0-9_]*)/'; 315 | $matches = []; 316 | $importableDependencies = []; 317 | if (preg_match_all($qualifiedClassesPattern, $placeholder, $matches)) { 318 | foreach ($matches[1] as $usedClass) { 319 | $namespacePieces = explode('\\', $usedClass); 320 | $className = array_pop($namespacePieces); 321 | 322 | /** 323 | * Avoid breaking same-model relationships when using base classes 324 | * 325 | * @see https://github.com/reliese/laravel/issues/209 326 | */ 327 | if ($model->usesBaseFiles() && $usedClass === $model->getQualifiedUserClassName()) { 328 | continue; 329 | } 330 | 331 | //When same class name but different namespace, skip it. 332 | if ( 333 | $className == $model->getClassName() && 334 | trim(implode('\\', $namespacePieces), '\\') != trim($model->getNamespace(), '\\') 335 | ) { 336 | continue; 337 | } 338 | 339 | $importableDependencies[trim($usedClass, '\\')] = true; 340 | $placeholder = preg_replace('!'.addslashes($usedClass).'\b!', addslashes($className), $placeholder, 1); 341 | } 342 | } 343 | 344 | return $importableDependencies; 345 | } 346 | 347 | /** 348 | * @param \Reliese\Coders\Model\Model $model 349 | * 350 | * @return string 351 | */ 352 | protected function properties(Model $model) 353 | { 354 | // Process property annotations 355 | $annotations = ''; 356 | 357 | foreach ($model->getProperties() as $name => $hint) { 358 | $annotations .= $this->class->annotation('property', "$hint \$$name"); 359 | } 360 | 361 | if ($model->hasRelations()) { 362 | // Add separation between model properties and model relations 363 | $annotations .= "\n * "; 364 | } 365 | 366 | foreach ($model->getRelations() as $name => $relation) { 367 | // TODO: Handle collisions, perhaps rename the relation. 368 | if ($model->hasProperty($name)) { 369 | continue; 370 | } 371 | $annotations .= $this->class->annotation('property', $relation->hint()." \$$name"); 372 | } 373 | 374 | return $annotations; 375 | } 376 | 377 | /** 378 | * @param \Reliese\Coders\Model\Model $model 379 | * 380 | * @return string 381 | */ 382 | protected function body(Model $model) 383 | { 384 | $body = ''; 385 | 386 | foreach ($model->getTraits() as $trait) { 387 | $body .= $this->class->mixin($trait); 388 | } 389 | 390 | $excludedConstants = []; 391 | 392 | if ($model->hasCustomCreatedAtField()) { 393 | $body .= $this->class->constant('CREATED_AT', $model->getCreatedAtField()); 394 | $excludedConstants[] = $model->getCreatedAtField(); 395 | } 396 | 397 | if ($model->hasCustomUpdatedAtField()) { 398 | $body .= $this->class->constant('UPDATED_AT', $model->getUpdatedAtField()); 399 | $excludedConstants[] = $model->getUpdatedAtField(); 400 | } 401 | 402 | if ($model->hasCustomDeletedAtField()) { 403 | $body .= $this->class->constant('DELETED_AT', $model->getDeletedAtField()); 404 | $excludedConstants[] = $model->getDeletedAtField(); 405 | } 406 | 407 | if ($model->usesPropertyConstants()) { 408 | // Take all properties and exclude already added constants with timestamps. 409 | $properties = array_keys($model->getProperties()); 410 | $properties = array_diff($properties, $excludedConstants); 411 | 412 | foreach ($properties as $property) { 413 | $constantName = Str::upper(Str::snake($property)); 414 | $body .= $this->class->constant($constantName, $property); 415 | } 416 | } 417 | 418 | $body = trim($body, "\n"); 419 | // Separate constants from fields only if there are constants. 420 | if (! empty($body)) { 421 | $body .= "\n"; 422 | } 423 | 424 | // Append connection name when required 425 | if ($model->shouldShowConnection()) { 426 | $body .= $this->class->field('connection', $model->getConnectionName()); 427 | } 428 | 429 | // When table is not plural, append the table name 430 | if ($model->needsTableName()) { 431 | $body .= $this->class->field('table', $model->getTableForQuery()); 432 | } 433 | 434 | if ($model->hasCustomPrimaryKey()) { 435 | $body .= $this->class->field('primaryKey', $model->getPrimaryKey()); 436 | } 437 | 438 | if ($model->doesNotAutoincrement()) { 439 | $body .= $this->class->field('incrementing', false, ['visibility' => 'public']); 440 | } 441 | 442 | if ($model->hasCustomPerPage()) { 443 | $body .= $this->class->field('perPage', $model->getPerPage()); 444 | } 445 | 446 | if (! $model->usesTimestamps()) { 447 | $body .= $this->class->field('timestamps', false, ['visibility' => 'public']); 448 | } 449 | 450 | if ($model->hasCustomDateFormat()) { 451 | $body .= $this->class->field('dateFormat', $model->getDateFormat()); 452 | } 453 | 454 | if ($model->doesNotUseSnakeAttributes()) { 455 | $body .= $this->class->field('snakeAttributes', false, ['visibility' => 'public static']); 456 | } 457 | 458 | if ($model->usesColumnList()) { 459 | $properties = array_keys($model->getProperties()); 460 | 461 | $body .= "\n"; 462 | $body .= $this->class->field('columns', $properties); 463 | } 464 | 465 | if ($model->hasCasts()) { 466 | $body .= $this->class->field('casts', $model->getCasts(), ['before' => "\n"]); 467 | } 468 | 469 | if ($model->hasHidden() && ($model->doesNotUseBaseFiles() || $model->hiddenInBaseFiles())) { 470 | $body .= $this->class->field('hidden', $model->getHidden(), ['before' => "\n"]); 471 | } 472 | 473 | if ($model->hasFillable() && ($model->doesNotUseBaseFiles() || $model->fillableInBaseFiles())) { 474 | $body .= $this->class->field('fillable', $model->getFillable(), ['before' => "\n"]); 475 | } 476 | 477 | if ($model->hasHints() && $model->usesHints()) { 478 | $body .= $this->class->field('hints', $model->getHints(), ['before' => "\n"]); 479 | } 480 | 481 | foreach ($model->getMutations() as $mutation) { 482 | $body .= $this->class->method($mutation->name(), $mutation->body(), ['before' => "\n"]); 483 | } 484 | 485 | foreach ($model->getRelations() as $constraint) { 486 | $body .= $this->class->method( 487 | $constraint->name(), 488 | $constraint->body(), 489 | [ 490 | 'before' => "\n", 491 | 'returnType' => $model->definesReturnTypes() ? $constraint->returnType() : null, 492 | ] 493 | ); 494 | } 495 | 496 | // Make sure there not undesired line breaks 497 | $body = trim($body, "\n"); 498 | 499 | return $body; 500 | } 501 | 502 | /** 503 | * @param \Reliese\Coders\Model\Model $model 504 | * @param array $custom 505 | * 506 | * @return string 507 | */ 508 | protected function modelPath(Model $model, $custom = []) 509 | { 510 | $modelsDirectory = $this->path(array_merge([$this->config($model->getBlueprint(), 'path')], $custom)); 511 | 512 | if (! $this->files->isDirectory($modelsDirectory)) { 513 | $this->files->makeDirectory($modelsDirectory, 0755, true); 514 | } 515 | 516 | return $this->path([$modelsDirectory, $model->getClassName().'.php']); 517 | } 518 | 519 | /** 520 | * @param array $pieces 521 | * 522 | * @return string 523 | */ 524 | protected function path($pieces) 525 | { 526 | return implode(DIRECTORY_SEPARATOR, (array) $pieces); 527 | } 528 | 529 | /** 530 | * @param \Reliese\Coders\Model\Model $model 531 | * 532 | * @return bool 533 | */ 534 | public function needsUserFile(Model $model) 535 | { 536 | return ! $this->files->exists($this->modelPath($model)) && $model->usesBaseFiles(); 537 | } 538 | 539 | /** 540 | * @param \Reliese\Coders\Model\Model $model 541 | * 542 | * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException 543 | */ 544 | protected function createUserFile(Model $model) 545 | { 546 | $file = $this->modelPath($model); 547 | 548 | $template = $this->prepareTemplate($model, 'user_model'); 549 | $template = str_replace('{{namespace}}', $model->getNamespace(), $template); 550 | $template = str_replace('{{class}}', $model->getClassName(), $template); 551 | $template = str_replace('{{imports}}', $this->formatBaseClasses($model), $template); 552 | $template = str_replace('{{parent}}', $this->getBaseClassName($model), $template); 553 | $template = str_replace('{{body}}', $this->userFileBody($model), $template); 554 | 555 | $this->files->put($file, $template); 556 | } 557 | 558 | /** 559 | * @param Model $model 560 | * @return string 561 | */ 562 | private function formatBaseClasses(Model $model) 563 | { 564 | return "use {$model->getBaseNamespace()}\\{$model->getClassName()} as {$this->getBaseClassName($model)};"; 565 | } 566 | 567 | /** 568 | * @param Model $model 569 | * @return string 570 | */ 571 | private function getBaseClassName(Model $model) 572 | { 573 | return 'Base'.$model->getClassName(); 574 | } 575 | 576 | /** 577 | * @param \Reliese\Coders\Model\Model $model 578 | * 579 | * @return string 580 | */ 581 | protected function userFileBody(Model $model) 582 | { 583 | $body = ''; 584 | 585 | if ($model->hasHidden() && !$model->hiddenInBaseFiles()) { 586 | $body .= $this->class->field('hidden', $model->getHidden()); 587 | } 588 | 589 | if ($model->hasFillable() && !$model->fillableInBaseFiles()) { 590 | $body .= $this->class->field('fillable', $model->getFillable(), ['before' => "\n"]); 591 | } 592 | 593 | // Make sure there is not an undesired line break at the end of the class body 594 | $body = ltrim(rtrim($body, "\n"), "\n"); 595 | 596 | return $body; 597 | } 598 | 599 | /** 600 | * @param \Reliese\Meta\Blueprint|null $blueprint 601 | * @param string $key 602 | * @param mixed $default 603 | * 604 | * @return mixed|\Reliese\Coders\Model\Config 605 | */ 606 | public function config(?Blueprint $blueprint = null, $key = null, $default = null) 607 | { 608 | if (is_null($blueprint)) { 609 | return $this->config; 610 | } 611 | 612 | return $this->config->get($blueprint, $key, $default); 613 | } 614 | } 615 | -------------------------------------------------------------------------------- /src/Coders/Model/Model.php: -------------------------------------------------------------------------------- 1 | blueprint = $blueprint; 176 | $this->factory = $factory; 177 | $this->loadRelations = $loadRelations; 178 | $this->mutators = $mutators; 179 | $this->configure(); 180 | $this->fill(); 181 | } 182 | 183 | protected function configure() 184 | { 185 | $this->withNamespace($this->config('namespace')); 186 | $this->withParentClass($this->config('parent')); 187 | 188 | // Timestamps settings 189 | $this->withTimestamps($this->config('timestamps.enabled', $this->config('timestamps', true))); 190 | $this->withCreatedAtField($this->config('timestamps.fields.CREATED_AT', $this->getDefaultCreatedAtField())); 191 | $this->withUpdatedAtField($this->config('timestamps.fields.UPDATED_AT', $this->getDefaultUpdatedAtField())); 192 | 193 | // Soft deletes settings 194 | $this->withSoftDeletes($this->config('soft_deletes.enabled', $this->config('soft_deletes', false))); 195 | $this->withDeletedAtField($this->config('soft_deletes.field', $this->getDefaultDeletedAtField())); 196 | 197 | // Connection settings 198 | $this->withConnection($this->config('connection', false)); 199 | $this->withConnectionName($this->blueprint->connection()); 200 | 201 | // Pagination settings 202 | $this->withPerPage($this->config('per_page', $this->getDefaultPerPage())); 203 | 204 | // Dates settings 205 | $this->withDateFormat($this->config('date_format', $this->getDefaultDateFormat())); 206 | 207 | // Table Prefix settings 208 | $this->withTablePrefix($this->config('table_prefix', $this->getDefaultTablePrefix())); 209 | 210 | // Relation name settings 211 | $this->withRelationNameStrategy($this->config('relation_name_strategy', $this->getDefaultRelationNameStrategy())); 212 | 213 | $this->definesReturnTypes = $this->config('enable_return_types', false); 214 | 215 | return $this; 216 | } 217 | 218 | /** 219 | * Parses the model information. 220 | */ 221 | protected function fill() 222 | { 223 | $this->primaryKeys = $this->blueprint->primaryKey(); 224 | 225 | // Process columns 226 | foreach ($this->blueprint->columns() as $column) { 227 | $this->parseColumn($column); 228 | } 229 | 230 | if (! $this->loadRelations) { 231 | return; 232 | } 233 | 234 | foreach ($this->blueprint->relations() as $relation) { 235 | $model = $this->makeRelationModel($relation); 236 | $belongsTo = new BelongsTo($relation, $this, $model); 237 | $this->relations[$belongsTo->name()] = $belongsTo; 238 | } 239 | 240 | foreach ($this->factory->referencing($this) as $related) { 241 | $factory = new ReferenceFactory($related, $this); 242 | $references = $factory->make(); 243 | foreach ($references as $reference) { 244 | $this->relations[$reference->name()] = $reference; 245 | } 246 | } 247 | } 248 | 249 | /** 250 | * @param \Illuminate\Support\Fluent $column 251 | */ 252 | protected function parseColumn(Fluent $column) 253 | { 254 | // TODO: Check type cast is OK 255 | $cast = $column->type; 256 | 257 | $propertyName = $this->usesPropertyConstants() ? 'self::'.strtoupper($column->name) : $column->name; 258 | 259 | // Due to some casting problems when converting null to a Carbon instance, 260 | // we are going to treat Soft Deletes field as string. 261 | if ($column->name == $this->getDeletedAtField()) { 262 | $cast = 'string'; 263 | } 264 | 265 | // Track attribute casts, ignoring timestamps 266 | if ($cast != 'string' && !in_array($propertyName, [$this->CREATED_AT, $this->UPDATED_AT])) { 267 | $this->casts[$propertyName] = $cast; 268 | } 269 | 270 | foreach ($this->config('casts', []) as $pattern => $casting) { 271 | if (Str::is($pattern, $column->name)) { 272 | $this->casts[$propertyName] = $cast = $casting; 273 | break; 274 | } 275 | } 276 | 277 | if ($this->isHidden($column->name)) { 278 | $this->hidden[] = $propertyName; 279 | } 280 | 281 | if ($this->isFillable($column->name)) { 282 | $this->fillable[] = $propertyName; 283 | } 284 | 285 | $this->mutate($column->name); 286 | 287 | // Track comment hints 288 | if (! empty($column->comment)) { 289 | $this->hints[$column->name] = $column->comment; 290 | } 291 | 292 | // Track PHP type hints 293 | $hint = $this->phpTypeHint($cast, $column->nullable); 294 | $this->properties[$column->name] = $hint; 295 | 296 | if ($column->name == $this->getPrimaryKey()) { 297 | $this->primaryKeyColumn = $column; 298 | } 299 | } 300 | 301 | /** 302 | * @param string $column 303 | */ 304 | protected function mutate($column) 305 | { 306 | foreach ($this->mutators as $mutator) { 307 | if ($mutator->applies($column, $this->getBlueprint())) { 308 | $this->mutations[] = new Mutation( 309 | $mutator->getName($column, $this), 310 | $mutator->getBody($column, $this) 311 | ); 312 | } 313 | } 314 | } 315 | 316 | /** 317 | * @param \Illuminate\Support\Fluent $relation 318 | * 319 | * @return $this|\Reliese\Coders\Model\Model 320 | */ 321 | public function makeRelationModel(Fluent $relation) 322 | { 323 | list($database, $table) = array_values($relation->on); 324 | 325 | if ($this->blueprint->is($database, $table)) { 326 | return $this; 327 | } 328 | 329 | return $this->factory->makeModel($database, $table, false); 330 | } 331 | 332 | /** 333 | * @param string $castType 334 | * @param bool $nullable 335 | * 336 | * @todo Make tests 337 | * 338 | * @return string 339 | */ 340 | public function phpTypeHint($castType, $nullable) 341 | { 342 | $type = $castType; 343 | 344 | switch ($castType) { 345 | case 'object': 346 | $type = '\stdClass'; 347 | break; 348 | case 'array': 349 | case 'json': 350 | $type = 'array'; 351 | break; 352 | case 'collection': 353 | $type = '\Illuminate\Support\Collection'; 354 | break; 355 | case 'datetime': 356 | $type = '\Carbon\Carbon'; 357 | break; 358 | case 'binary': 359 | $type = 'string'; 360 | break; 361 | } 362 | 363 | if ($nullable) { 364 | return $type.'|null'; 365 | } 366 | 367 | return $type; 368 | } 369 | 370 | /** 371 | * @return string 372 | */ 373 | public function getSchema() 374 | { 375 | return $this->blueprint->schema(); 376 | } 377 | 378 | /** 379 | * @return string 380 | */ 381 | public function getTable($andRemovePrefix = false) 382 | { 383 | if ($andRemovePrefix) { 384 | return $this->removeTablePrefix($this->blueprint->table()); 385 | } 386 | 387 | return $this->blueprint->table(); 388 | } 389 | 390 | /** 391 | * @return string 392 | */ 393 | public function getQualifiedTable() 394 | { 395 | return $this->blueprint->qualifiedTable(); 396 | } 397 | 398 | /** 399 | * @return string 400 | */ 401 | public function getTableForQuery() 402 | { 403 | return $this->shouldQualifyTableName() 404 | ? $this->getQualifiedTable() 405 | : $this->getTable(); 406 | } 407 | 408 | /** 409 | * @return bool 410 | */ 411 | public function shouldQualifyTableName() 412 | { 413 | return $this->config('qualified_tables', false); 414 | } 415 | 416 | /** 417 | * @return bool 418 | */ 419 | public function shouldPluralizeTableName() 420 | { 421 | $pluralize = (bool) $this->config('pluralize', true); 422 | 423 | $overridePluralizeFor = $this->config('override_pluralize_for', []); 424 | if (count($overridePluralizeFor) > 0) { 425 | foreach ($overridePluralizeFor as $except) { 426 | if ($except == $this->getTable()) { 427 | return ! $pluralize; 428 | } 429 | } 430 | } 431 | 432 | return $pluralize; 433 | } 434 | 435 | /** 436 | * @return bool 437 | */ 438 | public function shouldLowerCaseTableName() 439 | { 440 | return (bool) $this->config('lower_table_name_first', false); 441 | } 442 | 443 | /** 444 | * @param \Reliese\Meta\Blueprint[] $references 445 | */ 446 | public function withReferences($references) 447 | { 448 | $this->references = $references; 449 | } 450 | 451 | /** 452 | * @param string $namespace 453 | * 454 | * @return $this 455 | */ 456 | public function withNamespace($namespace) 457 | { 458 | $this->namespace = $namespace; 459 | 460 | return $this; 461 | } 462 | 463 | /** 464 | * @return string 465 | */ 466 | public function getNamespace() 467 | { 468 | return $this->namespace; 469 | } 470 | 471 | /** 472 | * @return string 473 | */ 474 | public function getRelationNameStrategy() 475 | { 476 | return $this->relationNameStrategy; 477 | } 478 | 479 | /** 480 | * @return string 481 | */ 482 | public function getBaseNamespace() 483 | { 484 | return $this->usesBaseFiles() 485 | ? $this->getNamespace().'\\Base' 486 | : $this->getNamespace(); 487 | } 488 | 489 | /** 490 | * @param string $parent 491 | * 492 | * @return $this 493 | */ 494 | public function withParentClass($parent) 495 | { 496 | $this->parentClass = '\\' . ltrim($parent, '\\'); 497 | 498 | return $this; 499 | } 500 | 501 | /** 502 | * @return string 503 | */ 504 | public function getParentClass() 505 | { 506 | return $this->parentClass; 507 | } 508 | 509 | /** 510 | * @return string 511 | */ 512 | public function getQualifiedUserClassName() 513 | { 514 | return '\\'.$this->getNamespace().'\\'.$this->getClassName(); 515 | } 516 | 517 | /** 518 | * @return string 519 | */ 520 | public function getClassName() 521 | { 522 | // Model names can be manually overridden by users in the config file. 523 | // If a config entry exists for this table, use that name, rather than generating one. 524 | $overriddenName = $this->config('model_names.' . $this->getTable()); 525 | if ($overriddenName) { 526 | return $overriddenName; 527 | } 528 | 529 | if ($this->shouldLowerCaseTableName()) { 530 | return Str::studly(Str::lower($this->getRecordName())); 531 | } 532 | 533 | return Str::studly($this->getRecordName()); 534 | } 535 | 536 | /** 537 | * @return string 538 | */ 539 | public function getRecordName() 540 | { 541 | if ($this->shouldPluralizeTableName()) { 542 | return Str::singular($this->removeTablePrefix($this->blueprint->table())); 543 | } 544 | 545 | return $this->removeTablePrefix($this->blueprint->table()); 546 | } 547 | 548 | /** 549 | * @param bool $timestampsEnabled 550 | * 551 | * @return $this 552 | */ 553 | public function withTimestamps($timestampsEnabled) 554 | { 555 | $this->timestamps = $timestampsEnabled; 556 | 557 | return $this; 558 | } 559 | 560 | /** 561 | * @return bool 562 | */ 563 | public function usesTimestamps() 564 | { 565 | return $this->timestamps && 566 | $this->blueprint->hasColumn($this->getCreatedAtField()) && 567 | $this->blueprint->hasColumn($this->getUpdatedAtField()); 568 | } 569 | 570 | /** 571 | * @param string $field 572 | * 573 | * @return $this 574 | */ 575 | public function withCreatedAtField($field) 576 | { 577 | $this->CREATED_AT = $field; 578 | 579 | return $this; 580 | } 581 | 582 | /** 583 | * @return string 584 | */ 585 | public function getCreatedAtField() 586 | { 587 | return $this->CREATED_AT; 588 | } 589 | 590 | /** 591 | * @return bool 592 | */ 593 | public function hasCustomCreatedAtField() 594 | { 595 | return $this->usesTimestamps() && 596 | $this->getCreatedAtField() != $this->getDefaultCreatedAtField(); 597 | } 598 | 599 | /** 600 | * @return string 601 | */ 602 | public function getDefaultCreatedAtField() 603 | { 604 | return Eloquent::CREATED_AT; 605 | } 606 | 607 | /** 608 | * @param string $field 609 | * 610 | * @return $this 611 | */ 612 | public function withUpdatedAtField($field) 613 | { 614 | $this->UPDATED_AT = $field; 615 | 616 | return $this; 617 | } 618 | 619 | /** 620 | * @return string 621 | */ 622 | public function getUpdatedAtField() 623 | { 624 | return $this->UPDATED_AT; 625 | } 626 | 627 | /** 628 | * @return bool 629 | */ 630 | public function hasCustomUpdatedAtField() 631 | { 632 | return $this->usesTimestamps() && 633 | $this->getUpdatedAtField() != $this->getDefaultUpdatedAtField(); 634 | } 635 | 636 | /** 637 | * @return string 638 | */ 639 | public function getDefaultUpdatedAtField() 640 | { 641 | return Eloquent::UPDATED_AT; 642 | } 643 | 644 | /** 645 | * @param bool $softDeletesEnabled 646 | * 647 | * @return $this 648 | */ 649 | public function withSoftDeletes($softDeletesEnabled) 650 | { 651 | $this->softDeletes = $softDeletesEnabled; 652 | 653 | return $this; 654 | } 655 | 656 | /** 657 | * @return bool 658 | */ 659 | public function usesSoftDeletes() 660 | { 661 | return $this->softDeletes && 662 | $this->blueprint->hasColumn($this->getDeletedAtField()); 663 | } 664 | 665 | /** 666 | * @param string $field 667 | * 668 | * @return $this 669 | */ 670 | public function withDeletedAtField($field) 671 | { 672 | $this->DELETED_AT = $field; 673 | 674 | return $this; 675 | } 676 | 677 | /** 678 | * @return string 679 | */ 680 | public function getDeletedAtField() 681 | { 682 | return $this->DELETED_AT; 683 | } 684 | 685 | /** 686 | * @return bool 687 | */ 688 | public function hasCustomDeletedAtField() 689 | { 690 | return $this->usesSoftDeletes() && 691 | $this->getDeletedAtField() != $this->getDefaultDeletedAtField(); 692 | } 693 | 694 | /** 695 | * @return string 696 | */ 697 | public function getDefaultDeletedAtField() 698 | { 699 | return 'deleted_at'; 700 | } 701 | 702 | /** 703 | * @return array 704 | */ 705 | public function getTraits() 706 | { 707 | $traits = $this->config('use', []); 708 | 709 | if (! is_array($traits)) { 710 | throw new \RuntimeException('Config use must be an array of valid traits to append to each model.'); 711 | } 712 | 713 | if ($this->usesSoftDeletes()) { 714 | $traits = array_merge([SoftDeletes::class], $traits); 715 | } 716 | 717 | return $traits; 718 | } 719 | 720 | /** 721 | * @return bool 722 | */ 723 | public function needsTableName() 724 | { 725 | return false === $this->shouldQualifyTableName() || 726 | $this->shouldRemoveTablePrefix() || 727 | $this->blueprint->table() != Str::plural($this->getRecordName()) || 728 | ! $this->shouldPluralizeTableName(); 729 | } 730 | 731 | /** 732 | * @return string 733 | */ 734 | public function shouldRemoveTablePrefix() 735 | { 736 | return ! empty($this->tablePrefix); 737 | } 738 | 739 | /** 740 | * @param string $tablePrefix 741 | */ 742 | public function withTablePrefix($tablePrefix) 743 | { 744 | $this->tablePrefix = $tablePrefix; 745 | } 746 | 747 | /** 748 | * @param string $relationNameStrategy 749 | */ 750 | public function withRelationNameStrategy($relationNameStrategy) 751 | { 752 | $this->relationNameStrategy = $relationNameStrategy; 753 | } 754 | 755 | /** 756 | * @param string $table 757 | */ 758 | public function removeTablePrefix($table) 759 | { 760 | if (($this->shouldRemoveTablePrefix()) && (substr($table, 0, strlen($this->tablePrefix)) == $this->tablePrefix)) { 761 | $table = substr($table, strlen($this->tablePrefix)); 762 | } 763 | 764 | return $table; 765 | } 766 | 767 | /** 768 | * @param bool $showConnection 769 | */ 770 | public function withConnection($showConnection) 771 | { 772 | $this->showConnection = $showConnection; 773 | } 774 | 775 | /** 776 | * @param string $connection 777 | */ 778 | public function withConnectionName($connection) 779 | { 780 | $this->connection = $connection; 781 | } 782 | 783 | /** 784 | * @return bool 785 | */ 786 | public function shouldShowConnection() 787 | { 788 | return (bool) $this->showConnection; 789 | } 790 | 791 | /** 792 | * @return string 793 | */ 794 | public function getConnectionName() 795 | { 796 | return $this->connection; 797 | } 798 | 799 | /** 800 | * @return bool 801 | */ 802 | public function hasCustomPrimaryKey() 803 | { 804 | return count($this->primaryKeys->columns) == 1 && 805 | $this->getPrimaryKey() != $this->getDefaultPrimaryKeyField(); 806 | } 807 | 808 | /** 809 | * @return string 810 | */ 811 | public function getDefaultPrimaryKeyField() 812 | { 813 | return 'id'; 814 | } 815 | 816 | /** 817 | * @todo: Improve it 818 | * @return string 819 | */ 820 | public function getPrimaryKey() 821 | { 822 | if (empty($this->primaryKeys->columns)) { 823 | return; 824 | } 825 | 826 | return $this->primaryKeys->columns[0]; 827 | } 828 | 829 | /** 830 | * @return string 831 | * @todo: check 832 | */ 833 | public function getPrimaryKeyType() 834 | { 835 | return $this->primaryKeyColumn->type; 836 | } 837 | 838 | /** 839 | * @todo: Check whether it is necessary 840 | * @return bool 841 | */ 842 | public function hasCustomPrimaryKeyCast() 843 | { 844 | return $this->getPrimaryKeyType() != $this->getDefaultPrimaryKeyType(); 845 | } 846 | 847 | /** 848 | * @return string 849 | */ 850 | public function getDefaultPrimaryKeyType() 851 | { 852 | return 'int'; 853 | } 854 | 855 | /** 856 | * @return bool 857 | */ 858 | public function doesNotAutoincrement() 859 | { 860 | return ! $this->autoincrement(); 861 | } 862 | 863 | /** 864 | * @return bool 865 | */ 866 | public function autoincrement() 867 | { 868 | if ($this->primaryKeyColumn) { 869 | return $this->primaryKeyColumn->autoincrement === true; 870 | } 871 | 872 | return false; 873 | } 874 | 875 | /** 876 | * @param $perPage 877 | */ 878 | public function withPerPage($perPage) 879 | { 880 | $this->perPage = (int) $perPage; 881 | } 882 | 883 | /** 884 | * @return int 885 | */ 886 | public function getPerPage() 887 | { 888 | return $this->perPage; 889 | } 890 | 891 | /** 892 | * @return bool 893 | */ 894 | public function hasCustomPerPage() 895 | { 896 | return $this->perPage != $this->getDefaultPerPage(); 897 | } 898 | 899 | /** 900 | * @return int 901 | */ 902 | public function getDefaultPerPage() 903 | { 904 | return 15; 905 | } 906 | 907 | /** 908 | * @param string $format 909 | * 910 | * @return $this 911 | */ 912 | public function withDateFormat($format) 913 | { 914 | $this->dateFormat = $format; 915 | 916 | return $this; 917 | } 918 | 919 | /** 920 | * @return string 921 | */ 922 | public function getDateFormat() 923 | { 924 | return $this->dateFormat; 925 | } 926 | 927 | /** 928 | * @return bool 929 | */ 930 | public function hasCustomDateFormat() 931 | { 932 | return $this->dateFormat != $this->getDefaultDateFormat(); 933 | } 934 | 935 | /** 936 | * @return string 937 | */ 938 | public function getDefaultDateFormat() 939 | { 940 | return 'Y-m-d H:i:s'; 941 | } 942 | 943 | /** 944 | * @return string 945 | */ 946 | public function getDefaultTablePrefix() 947 | { 948 | return ''; 949 | } 950 | 951 | /** 952 | * @return string 953 | */ 954 | public function getDefaultRelationNameStrategy() 955 | { 956 | return 'related'; 957 | } 958 | 959 | /** 960 | * @return bool 961 | */ 962 | public function hasCasts() 963 | { 964 | return ! empty($this->getCasts()); 965 | } 966 | 967 | /** 968 | * @return array 969 | */ 970 | public function getCasts() 971 | { 972 | if ( 973 | array_key_exists($this->getPrimaryKey(), $this->casts) && 974 | $this->autoincrement() 975 | ) { 976 | unset($this->casts[$this->getPrimaryKey()]); 977 | } 978 | 979 | return $this->casts; 980 | } 981 | 982 | /** 983 | * @return bool 984 | */ 985 | public function hasDates() 986 | { 987 | return ! empty($this->getDates()); 988 | } 989 | 990 | /** 991 | * @return array 992 | */ 993 | public function getDates() 994 | { 995 | return array_diff( 996 | array_filter($this->casts, function (string $cast) { 997 | return $cast === 'datetime'; 998 | }), 999 | [$this->CREATED_AT, $this->UPDATED_AT] 1000 | ); 1001 | } 1002 | 1003 | /** 1004 | * @return bool 1005 | */ 1006 | public function usesSnakeAttributes() 1007 | { 1008 | return (bool) $this->config('snake_attributes', true); 1009 | } 1010 | 1011 | /** 1012 | * @return bool 1013 | */ 1014 | public function doesNotUseSnakeAttributes() 1015 | { 1016 | return ! $this->usesSnakeAttributes(); 1017 | } 1018 | 1019 | /** 1020 | * @return bool 1021 | */ 1022 | public function hasHints() 1023 | { 1024 | return ! empty($this->getHints()); 1025 | } 1026 | 1027 | /** 1028 | * @return array 1029 | */ 1030 | public function getHints() 1031 | { 1032 | return $this->hints; 1033 | } 1034 | 1035 | /** 1036 | * @return array 1037 | */ 1038 | public function getProperties() 1039 | { 1040 | return $this->properties; 1041 | } 1042 | 1043 | /** 1044 | * @param string $name 1045 | * 1046 | * @return bool 1047 | */ 1048 | public function hasProperty($name) 1049 | { 1050 | return array_key_exists($name, $this->getProperties()); 1051 | } 1052 | 1053 | /** 1054 | * @return \Reliese\Coders\Model\Relation[] 1055 | */ 1056 | public function getRelations() 1057 | { 1058 | return $this->relations; 1059 | } 1060 | 1061 | /** 1062 | * @return bool 1063 | */ 1064 | public function hasRelations() 1065 | { 1066 | return ! empty($this->relations); 1067 | } 1068 | 1069 | /** 1070 | * @return \Reliese\Coders\Model\Mutation[] 1071 | */ 1072 | public function getMutations() 1073 | { 1074 | return $this->mutations; 1075 | } 1076 | 1077 | /** 1078 | * @param string $column 1079 | * 1080 | * @return bool 1081 | */ 1082 | public function isHidden($column) 1083 | { 1084 | $attributes = $this->config('hidden', []); 1085 | 1086 | if (! is_array($attributes)) { 1087 | throw new \RuntimeException('Config field [hidden] must be an array of attributes to hide from array or json.'); 1088 | } 1089 | 1090 | foreach ($attributes as $pattern) { 1091 | if (Str::is($pattern, $column)) { 1092 | return true; 1093 | } 1094 | } 1095 | 1096 | return false; 1097 | } 1098 | 1099 | /** 1100 | * @return bool 1101 | */ 1102 | public function hasHidden() 1103 | { 1104 | return ! empty($this->hidden); 1105 | } 1106 | 1107 | /** 1108 | * @return array 1109 | */ 1110 | public function getHidden() 1111 | { 1112 | return $this->hidden; 1113 | } 1114 | 1115 | /** 1116 | * @param string $column 1117 | * 1118 | * @return bool 1119 | */ 1120 | public function isFillable($column) 1121 | { 1122 | $guarded = $this->config('guarded', []); 1123 | 1124 | if (! is_array($guarded)) { 1125 | throw new \RuntimeException('Config field [guarded] must be an array of attributes to protect from mass assignment.'); 1126 | } 1127 | 1128 | $protected = [ 1129 | $this->getCreatedAtField(), 1130 | $this->getUpdatedAtField(), 1131 | $this->getDeletedAtField(), 1132 | ]; 1133 | 1134 | if ($this->primaryKeys->columns) { 1135 | $protected = array_merge($protected, $this->primaryKeys->columns); 1136 | } 1137 | 1138 | foreach (array_merge($guarded, $protected) as $pattern) { 1139 | if (Str::is($pattern, $column)) { 1140 | return false; 1141 | } 1142 | } 1143 | 1144 | return true; 1145 | } 1146 | 1147 | /** 1148 | * @return bool 1149 | */ 1150 | public function hasFillable() 1151 | { 1152 | return ! empty($this->fillable); 1153 | } 1154 | 1155 | /** 1156 | * @return array 1157 | */ 1158 | public function getFillable() 1159 | { 1160 | return $this->fillable; 1161 | } 1162 | 1163 | /** 1164 | * @return \Reliese\Meta\Blueprint 1165 | */ 1166 | public function getBlueprint() 1167 | { 1168 | return $this->blueprint; 1169 | } 1170 | 1171 | /** 1172 | * @param \Illuminate\Support\Fluent $command 1173 | * 1174 | * @return bool 1175 | */ 1176 | public function isPrimaryKey(Fluent $command) 1177 | { 1178 | foreach ((array) $this->primaryKeys->columns as $column) { 1179 | if (! in_array($column, $command->columns)) { 1180 | return false; 1181 | } 1182 | } 1183 | 1184 | return true; 1185 | } 1186 | 1187 | /** 1188 | * @param \Illuminate\Support\Fluent $command 1189 | * 1190 | * @return bool 1191 | */ 1192 | public function isUniqueKey(Fluent $command) 1193 | { 1194 | return $this->blueprint->isUniqueKey($command); 1195 | } 1196 | 1197 | /** 1198 | * @return bool 1199 | */ 1200 | public function usesBaseFiles() 1201 | { 1202 | return $this->config('base_files', false); 1203 | } 1204 | 1205 | /** 1206 | * @return bool 1207 | */ 1208 | public function usesPropertyConstants() 1209 | { 1210 | return $this->config('with_property_constants', false); 1211 | } 1212 | 1213 | public function usesColumnList() 1214 | { 1215 | return $this->config('with_column_list', false); 1216 | } 1217 | 1218 | /** 1219 | * @return int 1220 | */ 1221 | public function indentWithSpace() 1222 | { 1223 | return (int) $this->config('indent_with_space', 0); 1224 | } 1225 | 1226 | /** 1227 | * @return bool 1228 | */ 1229 | public function usesHints() 1230 | { 1231 | return $this->config('hints', false); 1232 | } 1233 | 1234 | /** 1235 | * @return bool 1236 | */ 1237 | public function doesNotUseBaseFiles() 1238 | { 1239 | return ! $this->usesBaseFiles(); 1240 | } 1241 | 1242 | /** 1243 | * @param string $key 1244 | * @param mixed $default 1245 | * 1246 | * @return mixed 1247 | */ 1248 | public function config($key = null, $default = null) 1249 | { 1250 | return $this->factory->config($this->getBlueprint(), $key, $default); 1251 | } 1252 | 1253 | /** 1254 | * @return bool 1255 | */ 1256 | public function fillableInBaseFiles(): bool 1257 | { 1258 | return $this->config('fillable_in_base_files', false); 1259 | } 1260 | 1261 | /** 1262 | * @return bool 1263 | */ 1264 | public function hiddenInBaseFiles(): bool 1265 | { 1266 | return $this->config('hidden_in_base_files', false); 1267 | } 1268 | 1269 | /** 1270 | * @return bool 1271 | */ 1272 | public function definesReturnTypes() 1273 | { 1274 | return $this->definesReturnTypes; 1275 | } 1276 | } 1277 | -------------------------------------------------------------------------------- /src/Coders/Model/ModelManager.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 34 | } 35 | 36 | /** 37 | * @param string $schema 38 | * @param string $table 39 | * @param \Reliese\Coders\Model\Mutator[] $mutators 40 | * @param bool $withRelations 41 | * 42 | * @return \Reliese\Coders\Model\Model 43 | */ 44 | public function make($schema, $table, $mutators = [], $withRelations = true) 45 | { 46 | $mapper = $this->factory->makeSchema($schema); 47 | 48 | $blueprint = $mapper->table($table); 49 | 50 | if (Arr::has($this->models, $blueprint->qualifiedTable())) { 51 | return $this->models[$schema][$table]; 52 | } 53 | 54 | $model = new Model($blueprint, $this->factory, $mutators, $withRelations); 55 | 56 | if ($withRelations) { 57 | $this->models[$schema][$table] = $model; 58 | } 59 | 60 | return $model; 61 | } 62 | 63 | /** 64 | * Get Models iterator. 65 | * 66 | * @return \ArrayIterator 67 | */ 68 | public function getIterator() 69 | { 70 | return new ArrayIterator($this->models); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Coders/Model/Mutation.php: -------------------------------------------------------------------------------- 1 | name = $name; 33 | $this->body = $body; 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function name() 40 | { 41 | return 'get'.Str::studly($this->name).'Attribute'; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function body() 48 | { 49 | return 'return '.$this->body.';'; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Coders/Model/Mutator.php: -------------------------------------------------------------------------------- 1 | condition = $condition; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * @param string $column 43 | * @param \Reliese\Meta\Blueprint $blueprint 44 | * 45 | * @return mixed 46 | */ 47 | public function applies($column, Blueprint $blueprint) 48 | { 49 | return call_user_func($this->condition, $column, $blueprint); 50 | } 51 | 52 | /** 53 | * @param \Closure $name 54 | * 55 | * @return $this 56 | */ 57 | public function name(\Closure $name) 58 | { 59 | $this->name = $name; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * @param string $attribute 66 | * @param \Reliese\Coders\Model\Model $model 67 | * 68 | * @return string 69 | */ 70 | public function getName($attribute, Model $model) 71 | { 72 | return call_user_func($this->name, $attribute, $model); 73 | } 74 | 75 | /** 76 | * @param \Closure $body 77 | * 78 | * @return $this 79 | */ 80 | public function body(\Closure $body) 81 | { 82 | $this->body = $body; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * @param string $attribute 89 | * @param \Reliese\Coders\Model\Model $model 90 | * 91 | * @return string 92 | */ 93 | public function getBody($attribute, Model $model) 94 | { 95 | return call_user_func($this->body, $attribute, $model); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Coders/Model/Relation.php: -------------------------------------------------------------------------------- 1 | command = $command; 43 | $this->parent = $parent; 44 | $this->related = $related; 45 | } 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function name() 51 | { 52 | switch ($this->parent->getRelationNameStrategy()) { 53 | case 'foreign_key': 54 | $relationName = RelationHelper::stripSuffixFromForeignKey( 55 | $this->parent->usesSnakeAttributes(), 56 | $this->otherKey(), 57 | $this->foreignKey() 58 | ); 59 | break; 60 | default: 61 | case 'related': 62 | $relationName = $this->related->getClassName(); 63 | break; 64 | } 65 | 66 | if ($this->parent->usesSnakeAttributes()) { 67 | return Str::snake($relationName); 68 | } 69 | 70 | return Str::camel($relationName); 71 | } 72 | 73 | /** 74 | * @return string 75 | */ 76 | public function body() 77 | { 78 | $body = 'return $this->belongsTo('; 79 | 80 | $body .= $this->related->getQualifiedUserClassName().'::class'; 81 | 82 | if ($this->needsForeignKey()) { 83 | $foreignKey = $this->parent->usesPropertyConstants() 84 | ? $this->parent->getQualifiedUserClassName().'::'.strtoupper($this->foreignKey()) 85 | : $this->foreignKey(); 86 | $body .= ', '.Dumper::export($foreignKey); 87 | } 88 | 89 | if ($this->needsOtherKey()) { 90 | $otherKey = $this->related->usesPropertyConstants() 91 | ? $this->related->getQualifiedUserClassName().'::'.strtoupper($this->otherKey()) 92 | : $this->otherKey(); 93 | $body .= ', '.Dumper::export($otherKey); 94 | } 95 | 96 | $body .= ')'; 97 | 98 | if ($this->hasCompositeOtherKey()) { 99 | // We will assume that when this happens the referenced columns are a composite primary key 100 | // or a composite unique key. Otherwise it should be a has-many relationship which is not 101 | // supported at the moment. @todo: Improve relationship resolution. 102 | foreach ($this->command->references as $index => $column) { 103 | $body .= "\n\t\t\t\t\t->where(". 104 | Dumper::export($this->qualifiedOtherKey($index)). 105 | ", '=', ". 106 | Dumper::export($this->qualifiedForeignKey($index)). 107 | ')'; 108 | } 109 | } 110 | 111 | $body .= ';'; 112 | 113 | return $body; 114 | } 115 | 116 | /** 117 | * @return string 118 | */ 119 | public function hint() 120 | { 121 | $base = $this->related->getQualifiedUserClassName(); 122 | 123 | if ($this->isNullable()) { 124 | $base .= '|null'; 125 | } 126 | 127 | return $base; 128 | } 129 | 130 | /** 131 | * @return string 132 | */ 133 | public function returnType() 134 | { 135 | return \Illuminate\Database\Eloquent\Relations\BelongsTo::class; 136 | } 137 | 138 | /** 139 | * @return bool 140 | */ 141 | protected function needsForeignKey() 142 | { 143 | $defaultForeignKey = $this->related->getRecordName().'_id'; 144 | 145 | return $defaultForeignKey != $this->foreignKey() || $this->needsOtherKey(); 146 | } 147 | 148 | /** 149 | * @param int $index 150 | * 151 | * @return string 152 | */ 153 | protected function foreignKey($index = 0) 154 | { 155 | return $this->command->columns[$index]; 156 | } 157 | 158 | /** 159 | * @param int $index 160 | * 161 | * @return string 162 | */ 163 | protected function qualifiedForeignKey($index = 0) 164 | { 165 | return $this->parent->getTable().'.'.$this->foreignKey($index); 166 | } 167 | 168 | /** 169 | * @return bool 170 | */ 171 | protected function needsOtherKey() 172 | { 173 | $defaultOtherKey = $this->related->getPrimaryKey(); 174 | 175 | return $defaultOtherKey != $this->otherKey(); 176 | } 177 | 178 | /** 179 | * @param int $index 180 | * 181 | * @return string 182 | */ 183 | protected function otherKey($index = 0) 184 | { 185 | return $this->command->references[$index]; 186 | } 187 | 188 | /** 189 | * @param int $index 190 | * 191 | * @return string 192 | */ 193 | protected function qualifiedOtherKey($index = 0) 194 | { 195 | return $this->related->getTable().'.'.$this->otherKey($index); 196 | } 197 | 198 | /** 199 | * Whether the "other key" is a composite foreign key. 200 | * 201 | * @return bool 202 | */ 203 | protected function hasCompositeOtherKey() 204 | { 205 | return count($this->command->references) > 1; 206 | } 207 | 208 | /** 209 | * @return bool 210 | */ 211 | private function isNullable() 212 | { 213 | return (bool) $this->parent->getBlueprint()->column($this->foreignKey())->get('nullable'); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Coders/Model/Relations/BelongsToMany.php: -------------------------------------------------------------------------------- 1 | parentCommand = $parentCommand; 61 | $this->referenceCommand = $referenceCommand; 62 | $this->parent = $parent; 63 | $this->pivot = $pivot; 64 | $this->reference = $reference; 65 | } 66 | 67 | /** 68 | * @return string 69 | */ 70 | public function hint() 71 | { 72 | return '\\'.Collection::class.'|'.$this->reference->getQualifiedUserClassName().'[]'; 73 | } 74 | 75 | /** 76 | * @return string 77 | */ 78 | public function name() 79 | { 80 | $tableName = $this->reference->getTable(true); 81 | 82 | if ($this->parent->shouldLowerCaseTableName()) { 83 | $tableName = strtolower($tableName); 84 | } 85 | if ($this->parent->shouldPluralizeTableName()) { 86 | $tableName = Str::plural(Str::singular($tableName)); 87 | } 88 | if ($this->parent->usesSnakeAttributes()) { 89 | return Str::snake($tableName); 90 | } 91 | 92 | return Str::camel($tableName); 93 | } 94 | 95 | /** 96 | * @return string 97 | */ 98 | public function body() 99 | { 100 | $body = 'return $this->belongsToMany('; 101 | 102 | $body .= $this->reference->getQualifiedUserClassName().'::class'; 103 | 104 | if ($this->needsPivotTable()) { 105 | $body .= ', '.Dumper::export($this->pivotTable()); 106 | } 107 | 108 | if ($this->needsForeignKey()) { 109 | $foreignKey = $this->parent->usesPropertyConstants() 110 | ? $this->reference->getQualifiedUserClassName().'::'.strtoupper($this->foreignKey()) 111 | : $this->foreignKey(); 112 | $body .= ', '.Dumper::export($foreignKey); 113 | } 114 | 115 | if ($this->needsOtherKey()) { 116 | $otherKey = $this->reference->usesPropertyConstants() 117 | ? $this->reference->getQualifiedUserClassName().'::'.strtoupper($this->otherKey()) 118 | : $this->otherKey(); 119 | $body .= ', '.Dumper::export($otherKey); 120 | } 121 | 122 | $body .= ')'; 123 | 124 | $fields = $this->getPivotFields(); 125 | 126 | if (! empty($fields)) { 127 | $body .= "\n\t\t\t\t\t->withPivot(".$this->parametrize($fields).')'; 128 | } 129 | 130 | if ($this->pivot->usesTimestamps()) { 131 | $body .= "\n\t\t\t\t\t->withTimestamps()"; 132 | } 133 | 134 | $body .= ';'; 135 | 136 | return $body; 137 | } 138 | 139 | /** 140 | * @return string 141 | */ 142 | public function returnType() 143 | { 144 | return \Illuminate\Database\Eloquent\Relations\BelongsToMany::class; 145 | } 146 | 147 | /** 148 | * @return bool 149 | */ 150 | protected function needsPivotTable() 151 | { 152 | $models = [$this->referenceRecordName(), $this->parentRecordName()]; 153 | sort($models); 154 | $defaultPivotTable = strtolower(implode('_', $models)); 155 | 156 | return $this->pivotTable() != $defaultPivotTable || $this->needsForeignKey(); 157 | } 158 | 159 | /** 160 | * @return mixed 161 | */ 162 | protected function pivotTable() 163 | { 164 | if ($this->parent->getSchema() != $this->pivot->getSchema()) { 165 | return $this->pivot->getQualifiedTable(); 166 | } 167 | 168 | return $this->pivot->getTable(); 169 | } 170 | 171 | /** 172 | * @return bool 173 | */ 174 | protected function needsForeignKey() 175 | { 176 | $defaultForeignKey = $this->parentRecordName().'_id'; 177 | 178 | return $this->foreignKey() != $defaultForeignKey || $this->needsOtherKey(); 179 | } 180 | 181 | /** 182 | * @return string 183 | */ 184 | protected function foreignKey() 185 | { 186 | return $this->parentCommand->columns[0]; 187 | } 188 | 189 | /** 190 | * @return bool 191 | */ 192 | protected function needsOtherKey() 193 | { 194 | $defaultOtherKey = $this->referenceRecordName().'_id'; 195 | 196 | return $this->otherKey() != $defaultOtherKey; 197 | } 198 | 199 | /** 200 | * @return string 201 | */ 202 | protected function otherKey() 203 | { 204 | return $this->referenceCommand->columns[0]; 205 | } 206 | 207 | private function getPivotFields() 208 | { 209 | return array_diff(array_keys($this->pivot->getProperties()), [ 210 | $this->foreignKey(), 211 | $this->otherKey(), 212 | $this->pivot->getCreatedAtField(), 213 | $this->pivot->getUpdatedAtField(), 214 | ]); 215 | } 216 | 217 | /** 218 | * @return string 219 | */ 220 | protected function parentRecordName() 221 | { 222 | // We make sure it is snake case because Eloquent assumes it is. 223 | return Str::snake($this->parent->getRecordName()); 224 | } 225 | 226 | /** 227 | * @return string 228 | */ 229 | protected function referenceRecordName() 230 | { 231 | // We make sure it is snake case because Eloquent assumes it is. 232 | return Str::snake($this->reference->getRecordName()); 233 | } 234 | 235 | /** 236 | * @param array $fields 237 | * 238 | * @return string 239 | */ 240 | private function parametrize($fields = []) 241 | { 242 | return (string) implode(', ', array_map(function ($field) { 243 | $field = $this->reference->usesPropertyConstants() 244 | ? $this->pivot->getQualifiedUserClassName().'::'.strtoupper($field) 245 | : $field; 246 | 247 | return Dumper::export($field); 248 | }, $fields)); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/Coders/Model/Relations/HasMany.php: -------------------------------------------------------------------------------- 1 | related->getQualifiedUserClassName().'[]'; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function name() 27 | { 28 | switch ($this->parent->getRelationNameStrategy()) { 29 | case 'foreign_key': 30 | $relationName = RelationHelper::stripSuffixFromForeignKey( 31 | $this->parent->usesSnakeAttributes(), 32 | $this->localKey(), 33 | $this->foreignKey() 34 | ); 35 | if (Str::snake($relationName) === Str::snake($this->parent->getClassName())) { 36 | $relationName = Str::plural($this->related->getClassName()); 37 | } else { 38 | $relationName = Str::plural($this->related->getClassName()) . 'Where' . ucfirst(Str::singular($relationName)); 39 | } 40 | break; 41 | default: 42 | case 'related': 43 | $relationName = Str::plural($this->related->getClassName()); 44 | break; 45 | } 46 | 47 | if ($this->parent->usesSnakeAttributes()) { 48 | return Str::snake($relationName); 49 | } 50 | 51 | return Str::camel($relationName); 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function method() 58 | { 59 | return 'hasMany'; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function returnType() 66 | { 67 | return \Illuminate\Database\Eloquent\Relations\HasMany::class; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Coders/Model/Relations/HasOne.php: -------------------------------------------------------------------------------- 1 | related->getQualifiedUserClassName() . '|null'; 20 | } 21 | 22 | /** 23 | * @return string 24 | */ 25 | public function name() 26 | { 27 | if ($this->parent->usesSnakeAttributes()) { 28 | return Str::snake($this->related->getClassName()); 29 | } 30 | 31 | return Str::camel($this->related->getClassName()); 32 | } 33 | 34 | /** 35 | * @return string 36 | */ 37 | public function method() 38 | { 39 | return 'hasOne'; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function returnType() 46 | { 47 | return \Illuminate\Database\Eloquent\Relations\HasOne::class; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Coders/Model/Relations/HasOneOrMany.php: -------------------------------------------------------------------------------- 1 | command = $command; 42 | $this->parent = $parent; 43 | $this->related = $related; 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | abstract public function hint(); 50 | 51 | /** 52 | * @return string 53 | */ 54 | abstract public function name(); 55 | 56 | /** 57 | * @return string 58 | */ 59 | public function body() 60 | { 61 | $body = 'return $this->'.$this->method().'('; 62 | 63 | $body .= $this->related->getQualifiedUserClassName().'::class'; 64 | 65 | if ($this->needsForeignKey()) { 66 | $foreignKey = $this->parent->usesPropertyConstants() 67 | ? $this->related->getQualifiedUserClassName().'::'.strtoupper($this->foreignKey()) 68 | : $this->foreignKey(); 69 | $body .= ', '.Dumper::export($foreignKey); 70 | } 71 | 72 | if ($this->needsLocalKey()) { 73 | $localKey = $this->related->usesPropertyConstants() 74 | ? $this->related->getQualifiedUserClassName().'::'.strtoupper($this->localKey()) 75 | : $this->localKey(); 76 | $body .= ', '.Dumper::export($localKey); 77 | } 78 | 79 | $body .= ');'; 80 | 81 | return $body; 82 | } 83 | 84 | /** 85 | * @return string 86 | */ 87 | abstract protected function method(); 88 | 89 | /** 90 | * @return bool 91 | */ 92 | protected function needsForeignKey() 93 | { 94 | $defaultForeignKey = $this->parent->getRecordName().'_id'; 95 | 96 | return $defaultForeignKey != $this->foreignKey() || $this->needsLocalKey(); 97 | } 98 | 99 | /** 100 | * @return string 101 | */ 102 | protected function foreignKey() 103 | { 104 | return $this->command->columns[0]; 105 | } 106 | 107 | /** 108 | * @return bool 109 | */ 110 | protected function needsLocalKey() 111 | { 112 | return $this->parent->getPrimaryKey() != $this->localKey(); 113 | } 114 | 115 | /** 116 | * @return string 117 | */ 118 | protected function localKey() 119 | { 120 | return $this->command->references[0]; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Coders/Model/Relations/HasOneOrManyStrategy.php: -------------------------------------------------------------------------------- 1 | isPrimaryKey($command) || 32 | $related->isUniqueKey($command) 33 | ) { 34 | $this->relation = new HasOne($command, $parent, $related); 35 | } else { 36 | $this->relation = new HasMany($command, $parent, $related); 37 | } 38 | } 39 | 40 | /** 41 | * @return string 42 | */ 43 | public function hint() 44 | { 45 | return $this->relation->hint(); 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function name() 52 | { 53 | return $this->relation->name(); 54 | } 55 | 56 | /** 57 | * @return string 58 | */ 59 | public function body() 60 | { 61 | return $this->relation->body(); 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function returnType() 68 | { 69 | return get_class($this->relation) === HasMany::class ? 70 | \Illuminate\Database\Eloquent\Relations\HasMany::class : 71 | \Illuminate\Database\Eloquent\Relations\HasOne::class; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Coders/Model/Relations/ReferenceFactory.php: -------------------------------------------------------------------------------- 1 | related = (array) $related; 38 | $this->parent = $parent; 39 | } 40 | 41 | /** 42 | * @return \Reliese\Coders\Model\Relation[] 43 | */ 44 | public function make() 45 | { 46 | if ($this->hasPivot()) { 47 | $relations = []; 48 | 49 | foreach ($this->references as $reference) { 50 | $relation = new BelongsToMany($this->getRelatedReference(), $reference['command'], $this->parent, $this->getRelatedModel(), $reference['model']); 51 | $relations[$relation->name()] = $relation; 52 | } 53 | 54 | return $relations; 55 | } 56 | 57 | return [new HasOneOrManyStrategy($this->getRelatedReference(), $this->parent, $this->getRelatedModel())]; 58 | } 59 | 60 | /** 61 | * @return bool 62 | */ 63 | protected function hasPivot() 64 | { 65 | $pivot = $this->getRelatedBlueprint()->table(); 66 | $firstRecord = $this->parent->getRecordName(); 67 | 68 | // See whether this potencial pivot table has the parent record name in it. 69 | // Not sure whether we should only take into account composite primary keys. 70 | if ( 71 | ! Str::contains($pivot, $firstRecord) 72 | ) { 73 | return false; 74 | } 75 | 76 | $pivot = preg_replace("!$firstRecord!", '', $pivot, 1); 77 | 78 | foreach ($this->getRelatedBlueprint()->relations() as $reference) { 79 | if ($reference == $this->getRelatedReference()) { 80 | continue; 81 | } 82 | 83 | $target = $this->getRelatedModel()->makeRelationModel($reference); 84 | 85 | // Check whether this potential pivot table has the target record name in it 86 | if (Str::contains($pivot, $target->getRecordName())) { 87 | $this->references[] = [ 88 | 'command' => $reference, 89 | 'model' => $target, 90 | ]; 91 | } 92 | } 93 | 94 | return count($this->references) > 0; 95 | } 96 | 97 | /** 98 | * @return \Illuminate\Support\Fluent 99 | */ 100 | protected function getRelatedReference() 101 | { 102 | return $this->related['reference']; 103 | } 104 | 105 | /** 106 | * @return \Reliese\Coders\Model\Model 107 | */ 108 | protected function getRelatedModel() 109 | { 110 | return $this->related['model']; 111 | } 112 | 113 | /** 114 | * @return \Reliese\Meta\Blueprint 115 | */ 116 | protected function getRelatedBlueprint() 117 | { 118 | return $this->related['blueprint']; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Coders/Model/Relations/RelationHelper.php: -------------------------------------------------------------------------------- 1 | fromBool($value); 37 | } 38 | 39 | /** 40 | * @param mixed $value 41 | * 42 | * @return mixed 43 | */ 44 | public function toBool($value) 45 | { 46 | if ($value === false) { 47 | return "\x00"; 48 | } 49 | if ($value === true) { 50 | return "\x01"; 51 | } 52 | 53 | return $value; 54 | } 55 | 56 | /** 57 | * @param mixed $value 58 | * 59 | * @return mixed 60 | */ 61 | public function toBoolean($value) 62 | { 63 | return $this->toBool($value); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Database/Eloquent/BlamableBehavior.php: -------------------------------------------------------------------------------- 1 | hasCustomGetCaster($key)) { 18 | return $this->{$this->getCustomGetCaster($key)}($value); 19 | } 20 | 21 | return parent::castAttribute($key, $value); 22 | } 23 | 24 | /** 25 | * @param string $key 26 | * 27 | * @return bool 28 | */ 29 | protected function hasCustomGetCaster($key) 30 | { 31 | return $this->hasCast($key) && method_exists($this, $this->getCustomGetCaster($key)); 32 | } 33 | 34 | /** 35 | * @param string $key 36 | * 37 | * @return string 38 | */ 39 | protected function getCustomGetCaster($key) 40 | { 41 | return 'from'.ucfirst($this->getCastType($key)); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function setAttribute($key, $value) 48 | { 49 | if ($this->hasCustomSetCaster($key)) { 50 | $value = $this->{$this->getCustomSetCaster($key)}($value); 51 | } 52 | 53 | return parent::setAttribute($key, $value); 54 | } 55 | 56 | /** 57 | * @param string $key 58 | * 59 | * @return bool 60 | */ 61 | private function hasCustomSetCaster($key) 62 | { 63 | return $this->hasCast($key) && method_exists($this, $this->getCustomSetCaster($key)); 64 | } 65 | 66 | /** 67 | * @param string $key 68 | * 69 | * @return string 70 | */ 71 | private function getCustomSetCaster($key) 72 | { 73 | return 'to'.ucfirst($this->getCastType($key)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Database/Eloquent/WhoDidIt.php: -------------------------------------------------------------------------------- 1 | request = $request; 28 | } 29 | 30 | /** 31 | * @param \Illuminate\Database\Eloquent\Model $model 32 | */ 33 | public function creating(Eloquent $model) 34 | { 35 | $model->created_by = $this->doer(); 36 | } 37 | 38 | /** 39 | * @param \Illuminate\Database\Eloquent\Model $model 40 | */ 41 | public function updating(Eloquent $model) 42 | { 43 | $model->updated_by = $this->doer(); 44 | } 45 | 46 | /** 47 | * @return mixed|string 48 | */ 49 | protected function doer() 50 | { 51 | if (app()->runningInConsole()) { 52 | return 'CLI'; 53 | } 54 | 55 | return $this->authenticated() ? $this->userId() : '????'; 56 | } 57 | 58 | /** 59 | * @return mixed 60 | */ 61 | protected function authenticated() 62 | { 63 | return $this->request->user(); 64 | } 65 | 66 | /** 67 | * @return mixed 68 | */ 69 | protected function userId() 70 | { 71 | return $this->authenticated()->id; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Meta/Blueprint.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 69 | $this->schema = $schema; 70 | $this->table = $table; 71 | $this->isView = $isView; 72 | } 73 | 74 | /** 75 | * @return string 76 | */ 77 | public function schema() 78 | { 79 | return $this->schema; 80 | } 81 | 82 | /** 83 | * @return string 84 | */ 85 | public function table() 86 | { 87 | return $this->table; 88 | } 89 | 90 | /** 91 | * @return string 92 | */ 93 | public function qualifiedTable() 94 | { 95 | return $this->schema().'.'.$this->table(); 96 | } 97 | 98 | /** 99 | * @param \Illuminate\Support\Fluent $column 100 | * 101 | * @return $this 102 | */ 103 | public function withColumn(Fluent $column) 104 | { 105 | $this->columns[$column->name] = $column; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * @return \Illuminate\Support\Fluent[] 112 | */ 113 | public function columns() 114 | { 115 | return $this->columns; 116 | } 117 | 118 | /** 119 | * @param string $name 120 | * 121 | * @return bool 122 | */ 123 | public function hasColumn($name) 124 | { 125 | return array_key_exists($name, $this->columns); 126 | } 127 | 128 | /** 129 | * @param string $name 130 | * 131 | * @return \Illuminate\Support\Fluent 132 | */ 133 | public function column($name) 134 | { 135 | if (! $this->hasColumn($name)) { 136 | throw new \InvalidArgumentException("Column [$name] does not belong to table [{$this->qualifiedTable()}]"); 137 | } 138 | 139 | return $this->columns[$name]; 140 | } 141 | 142 | /** 143 | * @param \Illuminate\Support\Fluent $index 144 | * 145 | * @return $this 146 | */ 147 | public function withIndex(Fluent $index) 148 | { 149 | $this->indexes[] = $index; 150 | 151 | if ($index->name == 'unique') { 152 | $this->unique[] = $index; 153 | } 154 | 155 | return $this; 156 | } 157 | 158 | /** 159 | * @return \Illuminate\Support\Fluent[] 160 | */ 161 | public function indexes() 162 | { 163 | return $this->indexes; 164 | } 165 | 166 | /** 167 | * @param \Illuminate\Support\Fluent $index 168 | * 169 | * @return $this 170 | */ 171 | public function withRelation(Fluent $index) 172 | { 173 | $this->relations[] = $index; 174 | 175 | return $this; 176 | } 177 | 178 | /** 179 | * @return \Illuminate\Support\Fluent[] 180 | */ 181 | public function relations() 182 | { 183 | return $this->relations; 184 | } 185 | 186 | /** 187 | * @param \Illuminate\Support\Fluent $primaryKey 188 | * 189 | * @return $this 190 | */ 191 | public function withPrimaryKey(Fluent $primaryKey) 192 | { 193 | $this->primaryKey = $primaryKey; 194 | 195 | return $this; 196 | } 197 | 198 | /** 199 | * @return \Illuminate\Support\Fluent 200 | */ 201 | public function primaryKey() 202 | { 203 | if ($this->primaryKey) { 204 | return $this->primaryKey; 205 | } 206 | 207 | if (! empty($this->unique)) { 208 | return current($this->unique); 209 | } 210 | 211 | $nullPrimaryKey = new Fluent(['columns' => []]); 212 | 213 | return $nullPrimaryKey; 214 | } 215 | 216 | /** 217 | * @return bool 218 | */ 219 | public function hasCompositePrimaryKey() 220 | { 221 | return count($this->primaryKey->columns) > 1; 222 | } 223 | 224 | /** 225 | * @return string 226 | */ 227 | public function connection() 228 | { 229 | return $this->connection; 230 | } 231 | 232 | /** 233 | * @param string $database 234 | * @param string $table 235 | * 236 | * @return bool 237 | */ 238 | public function is($database, $table) 239 | { 240 | return $database == $this->schema() && $table == $this->table(); 241 | } 242 | 243 | /** 244 | * @param \Reliese\Meta\Blueprint $table 245 | * 246 | * @return array 247 | */ 248 | public function references(self $table) 249 | { 250 | $references = []; 251 | 252 | foreach ($this->relations() as $relation) { 253 | list($foreignDatabase, $foreignTable) = array_values($relation->on); 254 | if ($table->is($foreignDatabase, $foreignTable)) { 255 | $references[] = $relation; 256 | } 257 | } 258 | 259 | return $references; 260 | } 261 | 262 | /** 263 | * @param \Illuminate\Support\Fluent $constraint 264 | * 265 | * @return bool 266 | */ 267 | public function isUniqueKey(Fluent $constraint) 268 | { 269 | foreach ($this->unique as $index) { 270 | 271 | // We only need to consider cases, when UNIQUE KEY is presented by only ONE column 272 | if (count($index->columns) === 1 && isset($index->columns[0])) { 273 | if (in_array($index->columns[0], $constraint->columns)) { 274 | return true; 275 | } 276 | } 277 | } 278 | 279 | return false; 280 | } 281 | 282 | /** 283 | * @return bool 284 | */ 285 | public function isView() 286 | { 287 | return $this->isView; 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/Meta/Column.php: -------------------------------------------------------------------------------- 1 | ['varchar', 'text', 'string', 'char', 'enum', 'set', 'tinytext', 'mediumtext', 'longtext', 'longblob', 'mediumblob', 'tinyblob', 'blob'], 33 | 'datetime' => ['datetime', 'year', 'date', 'time', 'timestamp'], 34 | 'int' => ['bigint', 'int', 'integer', 'tinyint', 'smallint', 'mediumint'], 35 | 'float' => ['float', 'decimal', 'numeric', 'dec', 'fixed', 'double', 'real', 'double precision'], 36 | 'boolean' => ['bit'] 37 | ]; 38 | 39 | /** 40 | * MysqlColumn constructor. 41 | * 42 | * @param array $metadata 43 | */ 44 | public function __construct($metadata = []) 45 | { 46 | $this->metadata = $metadata; 47 | } 48 | 49 | /** 50 | * @return \Illuminate\Support\Fluent 51 | */ 52 | public function normalize() 53 | { 54 | $attributes = new Fluent(); 55 | 56 | foreach ($this->metas as $meta) { 57 | $this->{'parse'.ucfirst($meta)}($attributes); 58 | } 59 | 60 | return $attributes; 61 | } 62 | 63 | /** 64 | * @param \Illuminate\Support\Fluent $attributes 65 | */ 66 | protected function parseType(Fluent $attributes) 67 | { 68 | $type = $this->get('Type', 'string'); 69 | 70 | preg_match('/^(\w+)(?:\(([^\)]+)\))?/', $type, $matches); 71 | 72 | $dataType = strtolower($matches[1]); 73 | $attributes['type'] = $dataType; 74 | 75 | foreach (static::$mappings as $phpType => $database) { 76 | if (in_array($dataType, $database)) { 77 | $attributes['type'] = $phpType; 78 | } 79 | } 80 | 81 | if (isset($matches[2])) { 82 | $this->parsePrecision($dataType, $matches[2], $attributes); 83 | } 84 | 85 | if ($attributes['type'] == 'int') { 86 | $attributes['unsigned'] = Str::contains($type, 'unsigned'); 87 | } 88 | } 89 | 90 | /** 91 | * @param string $databaseType 92 | * @param string $precision 93 | * @param \Illuminate\Support\Fluent $attributes 94 | */ 95 | protected function parsePrecision($databaseType, $precision, Fluent $attributes) 96 | { 97 | $precision = explode(',', str_replace("'", '', $precision)); 98 | 99 | // Check whether it's an enum 100 | if ($databaseType == 'enum') { 101 | $attributes['enum'] = $precision; 102 | 103 | return; 104 | } 105 | 106 | $size = (int) current($precision); 107 | 108 | // Check whether it's a boolean 109 | if ($size == 1 && in_array($databaseType, ['bit', 'tinyint'])) { 110 | // Make sure this column type is a boolean 111 | $attributes['type'] = 'bool'; 112 | 113 | if ($databaseType == 'bit') { 114 | $attributes['mappings'] = ["\x00" => false, "\x01" => true]; 115 | } 116 | 117 | return; 118 | } 119 | 120 | $attributes['size'] = $size; 121 | 122 | if ($scale = next($precision)) { 123 | $attributes['scale'] = (int) $scale; 124 | } 125 | } 126 | 127 | /** 128 | * @param \Illuminate\Support\Fluent $attributes 129 | */ 130 | protected function parseName(Fluent $attributes) 131 | { 132 | $attributes['name'] = $this->get('Field'); 133 | } 134 | 135 | /** 136 | * @param \Illuminate\Support\Fluent $attributes 137 | */ 138 | protected function parseAutoincrement(Fluent $attributes) 139 | { 140 | if ($this->same('Extra', 'auto_increment')) { 141 | $attributes['autoincrement'] = true; 142 | } 143 | } 144 | 145 | /** 146 | * @param \Illuminate\Support\Fluent $attributes 147 | */ 148 | protected function parseNullable(Fluent $attributes) 149 | { 150 | $attributes['nullable'] = $this->same('Null', 'YES'); 151 | } 152 | 153 | /** 154 | * @param \Illuminate\Support\Fluent $attributes 155 | */ 156 | protected function parseDefault(Fluent $attributes) 157 | { 158 | $attributes['default'] = $this->get('Default'); 159 | } 160 | 161 | /** 162 | * @param \Illuminate\Support\Fluent $attributes 163 | */ 164 | protected function parseComment(Fluent $attributes) 165 | { 166 | $attributes['comment'] = $this->get('Comment'); 167 | } 168 | 169 | /** 170 | * @param string $key 171 | * @param mixed $default 172 | * 173 | * @return mixed 174 | */ 175 | protected function get($key, $default = null) 176 | { 177 | return Arr::get($this->metadata, $key, $default); 178 | } 179 | 180 | /** 181 | * @param string $key 182 | * @param string $value 183 | * 184 | * @return bool 185 | */ 186 | protected function same($key, $value) 187 | { 188 | return strcasecmp($this->get($key, ''), $value) === 0; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Meta/MySql/Schema.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 45 | $this->connection = $connection; 46 | 47 | $this->load(); 48 | } 49 | 50 | /** 51 | * @return \Doctrine\DBAL\Schema\AbstractSchemaManager 52 | * @todo: Use Doctrine instead of raw database queries 53 | */ 54 | public function manager() 55 | { 56 | return $this->connection->getDoctrineSchemaManager(); 57 | } 58 | 59 | /** 60 | * Loads schema's tables' information from the database. 61 | */ 62 | protected function load() 63 | { 64 | $tables = $this->fetchTables($this->schema); 65 | foreach ($tables as $table) { 66 | $this->loadTable($table); 67 | } 68 | $views = $this->fetchViews($this->schema); 69 | foreach ($views as $table) { 70 | $this->loadTable($table, true); 71 | } 72 | } 73 | 74 | /** 75 | * @param string $schema 76 | * 77 | * @return array 78 | */ 79 | protected function fetchTables($schema) 80 | { 81 | $rows = $this->arraify($this->connection->select('SHOW FULL TABLES FROM '.$this->wrap($schema).' WHERE Table_type="BASE TABLE"')); 82 | $names = array_column($rows, 'Tables_in_'.$schema); 83 | 84 | return Arr::flatten($names); 85 | } 86 | 87 | /** 88 | * @param string $schema 89 | * 90 | * @return array 91 | */ 92 | protected function fetchViews($schema) 93 | { 94 | $rows = $this->arraify($this->connection->select('SHOW FULL TABLES FROM '.$this->wrap($schema).' WHERE Table_type="VIEW"')); 95 | $names = array_column($rows, 'Tables_in_'.$schema); 96 | 97 | return Arr::flatten($names); 98 | } 99 | 100 | /** 101 | * @param \Reliese\Meta\Blueprint $blueprint 102 | */ 103 | protected function fillColumns(Blueprint $blueprint) 104 | { 105 | $rows = $this->arraify($this->connection->select('SHOW FULL COLUMNS FROM '.$this->wrap($blueprint->qualifiedTable()))); 106 | foreach ($rows as $column) { 107 | $blueprint->withColumn( 108 | $this->parseColumn($column) 109 | ); 110 | } 111 | } 112 | 113 | /** 114 | * @param array $metadata 115 | * 116 | * @return \Illuminate\Support\Fluent 117 | */ 118 | protected function parseColumn($metadata) 119 | { 120 | return (new Column($metadata))->normalize(); 121 | } 122 | 123 | /** 124 | * @param \Reliese\Meta\Blueprint $blueprint 125 | */ 126 | protected function fillConstraints(Blueprint $blueprint) 127 | { 128 | $row = $this->arraify($this->connection->select('SHOW CREATE TABLE '.$this->wrap($blueprint->qualifiedTable()))); 129 | $row = array_change_key_case($row[0]); 130 | $sql = ($blueprint->isView() ? $row['create view'] : $row['create table']); 131 | $sql = str_replace('`', '', $sql); 132 | 133 | $this->fillPrimaryKey($sql, $blueprint); 134 | $this->fillIndexes($sql, $blueprint); 135 | $this->fillRelations($sql, $blueprint); 136 | } 137 | 138 | /** 139 | * Quick little hack since it is no longer possible to set PDO's fetch mode 140 | * to PDO::FETCH_ASSOC. 141 | * 142 | * @param $data 143 | * @return mixed 144 | */ 145 | protected function arraify($data) 146 | { 147 | return json_decode(json_encode($data), true); 148 | } 149 | 150 | /** 151 | * @param string $sql 152 | * @param \Reliese\Meta\Blueprint $blueprint 153 | * @todo: Support named primary keys 154 | */ 155 | protected function fillPrimaryKey($sql, Blueprint $blueprint) 156 | { 157 | $pattern = '/\s*(PRIMARY KEY)\s+\(([^\)]+)\)/mi'; 158 | if (preg_match_all($pattern, $sql, $indexes, PREG_SET_ORDER) == false) { 159 | return; 160 | } 161 | 162 | $key = [ 163 | 'name' => 'primary', 164 | 'index' => '', 165 | 'columns' => $this->columnize($indexes[0][2]), 166 | ]; 167 | 168 | $blueprint->withPrimaryKey(new Fluent($key)); 169 | } 170 | 171 | /** 172 | * @param string $sql 173 | * @param \Reliese\Meta\Blueprint $blueprint 174 | */ 175 | protected function fillIndexes($sql, Blueprint $blueprint) 176 | { 177 | $pattern = '/\s*(UNIQUE)?\s*(KEY|INDEX)\s+(\w+)\s+\(([^\)]+)\)/mi'; 178 | if (preg_match_all($pattern, $sql, $indexes, PREG_SET_ORDER) == false) { 179 | return; 180 | } 181 | 182 | foreach ($indexes as $setup) { 183 | $index = [ 184 | 'name' => strcasecmp($setup[1], 'unique') === 0 ? 'unique' : 'index', 185 | 'columns' => $this->columnize($setup[4]), 186 | 'index' => $setup[3], 187 | ]; 188 | $blueprint->withIndex(new Fluent($index)); 189 | } 190 | } 191 | 192 | /** 193 | * @param string $sql 194 | * @param \Reliese\Meta\Blueprint $blueprint 195 | * @todo: Support named foreign keys 196 | */ 197 | protected function fillRelations($sql, Blueprint $blueprint) 198 | { 199 | $pattern = '/FOREIGN KEY\s+\(([^\)]+)\)\s+REFERENCES\s+([^\(^\s]+)\s*\(([^\)]+)\)/mi'; 200 | preg_match_all($pattern, $sql, $relations, PREG_SET_ORDER); 201 | 202 | foreach ($relations as $setup) { 203 | $table = $this->resolveForeignTable($setup[2], $blueprint); 204 | 205 | $relation = [ 206 | 'name' => 'foreign', 207 | 'index' => '', 208 | 'columns' => $this->columnize($setup[1]), 209 | 'references' => $this->columnize($setup[3]), 210 | 'on' => $table, 211 | ]; 212 | 213 | $blueprint->withRelation(new Fluent($relation)); 214 | } 215 | } 216 | 217 | /** 218 | * @param string $columns 219 | * 220 | * @return array 221 | */ 222 | protected function columnize($columns) 223 | { 224 | return array_map('trim', explode(',', $columns)); 225 | } 226 | 227 | /** 228 | * Wrap within backticks. 229 | * 230 | * @param string $table 231 | * 232 | * @return string 233 | */ 234 | protected function wrap($table) 235 | { 236 | $pieces = explode('.', str_replace('`', '', $table)); 237 | 238 | return implode('.', array_map(function ($piece) { 239 | return "`$piece`"; 240 | }, $pieces)); 241 | } 242 | 243 | /** 244 | * @param string $table 245 | * @param \Reliese\Meta\Blueprint $blueprint 246 | * 247 | * @return array 248 | */ 249 | protected function resolveForeignTable($table, Blueprint $blueprint) 250 | { 251 | $referenced = explode('.', $table); 252 | 253 | if (count($referenced) == 2) { 254 | return [ 255 | 'database' => current($referenced), 256 | 'table' => next($referenced), 257 | ]; 258 | } 259 | 260 | return [ 261 | 'database' => $blueprint->schema(), 262 | 'table' => current($referenced), 263 | ]; 264 | } 265 | 266 | /** 267 | * @param \Illuminate\Database\Connection $connection 268 | * 269 | * @return array 270 | */ 271 | public static function schemas(Connection $connection) 272 | { 273 | $schemas = $connection->select('SELECT schema_name FROM information_schema.schemata'); 274 | $schemas = array_column($schemas, 'schema_name'); 275 | return array_diff($schemas, [ 276 | 'information_schema', 277 | 'sys', 278 | 'mysql', 279 | 'performance_schema', 280 | ]); 281 | } 282 | 283 | /** 284 | * @return string 285 | */ 286 | public function schema() 287 | { 288 | return $this->schema; 289 | } 290 | 291 | /** 292 | * @param string $table 293 | * 294 | * @return bool 295 | */ 296 | public function has($table) 297 | { 298 | return array_key_exists($table, $this->tables); 299 | } 300 | 301 | /** 302 | * @return \Reliese\Meta\Blueprint[] 303 | */ 304 | public function tables() 305 | { 306 | return $this->tables; 307 | } 308 | 309 | /** 310 | * @param string $table 311 | * 312 | * @return \Reliese\Meta\Blueprint 313 | */ 314 | public function table($table) 315 | { 316 | if (! $this->has($table)) { 317 | throw new \InvalidArgumentException("Table [$table] does not belong to schema [{$this->schema}]"); 318 | } 319 | 320 | return $this->tables[$table]; 321 | } 322 | 323 | /** 324 | * @return \Illuminate\Database\MySqlConnection 325 | */ 326 | public function connection() 327 | { 328 | return $this->connection; 329 | } 330 | 331 | /** 332 | * @param \Reliese\Meta\Blueprint $table 333 | * 334 | * @return array 335 | */ 336 | public function referencing(Blueprint $table) 337 | { 338 | $references = []; 339 | 340 | foreach ($this->tables as $blueprint) { 341 | foreach ($blueprint->references($table) as $reference) { 342 | $references[] = [ 343 | 'blueprint' => $blueprint, 344 | 'reference' => $reference, 345 | ]; 346 | } 347 | } 348 | 349 | return $references; 350 | } 351 | 352 | /** 353 | * @param string $table 354 | * @param bool $isView 355 | */ 356 | protected function loadTable($table, $isView = false) 357 | { 358 | $blueprint = new Blueprint($this->connection->getName(), $this->schema, $table, $isView); 359 | $this->fillColumns($blueprint); 360 | $this->fillConstraints($blueprint); 361 | $this->tables[$table] = $blueprint; 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/Meta/Postgres/Column.php: -------------------------------------------------------------------------------- 1 | ['character varying', 'varchar', 'text', 'string', 'char', 'character','enum', 'tinytext', 'mediumtext', 'longtext', 'json'], 32 | 'datetime' => ['timestamp with time zone', 'timestamp without time zone', 'timestamptz', 'datetime', 'year', 'date', 'time', 'timestamp'], 33 | 'int' => ['int', 'integer', 'tinyint', 'smallint', 'mediumint', 'bigint', 'bigserial', 'serial', 'smallserial', 'tinyserial', 'serial4', 'serial8'], 34 | 'float' => ['float', 'decimal', 'numeric', 'dec', 'fixed', 'double', 'real', 'double precision'], 35 | 'boolean' => ['boolean', 'bool', 'bit'], 36 | 'binary' => ['blob', 'longblob', 'jsonb'], 37 | ]; 38 | 39 | /** 40 | * PostgresColumn constructor. 41 | * 42 | * @param array $metadata 43 | */ 44 | public function __construct($metadata = []) 45 | { 46 | $this->metadata = $metadata; 47 | } 48 | 49 | /** 50 | * @return \Illuminate\Support\Fluent 51 | */ 52 | public function normalize() 53 | { 54 | $attributes = new Fluent(); 55 | 56 | foreach ($this->metas as $meta) { 57 | $this->{'parse'.ucfirst($meta)}($attributes); 58 | } 59 | 60 | return $attributes; 61 | } 62 | 63 | /** 64 | * @param \Illuminate\Support\Fluent $attributes 65 | */ 66 | protected function parseType(Fluent $attributes) 67 | { 68 | $dataType = $this->get('data_type', 'string'); 69 | $attributes['type'] = $dataType; 70 | 71 | foreach (static::$mappings as $phpType => $database) { 72 | if (in_array($dataType, $database)) { 73 | $attributes['type'] = $phpType; 74 | } 75 | } 76 | 77 | $this->parsePrecision($dataType, $attributes); 78 | } 79 | 80 | /** 81 | * @param string $databaseType 82 | * @param \Illuminate\Support\Fluent $attributes 83 | * @todo handle non numeric precisions 84 | */ 85 | protected function parsePrecision($databaseType, Fluent $attributes) 86 | { 87 | $precision = $this->get('numeric_precision', 'string'); 88 | $precision = explode(',', str_replace("'", '', $precision)); 89 | 90 | // Check whether it's an enum 91 | if ($databaseType == 'enum') { 92 | //$attributes['enum'] = $precision; //todo 93 | 94 | return; 95 | } 96 | 97 | $size = (int) $precision; 98 | 99 | // Check whether it's a boolean 100 | if ($size == 1 && in_array($databaseType, self::$mappings['boolean'])) { 101 | // Make sure this column type is a boolean 102 | $attributes['type'] = 'bool'; 103 | 104 | if ($databaseType == 'bit') { 105 | $attributes['mappings'] = ["\x00" => false, "\x01" => true]; 106 | } 107 | 108 | return; 109 | } 110 | 111 | $attributes['size'] = $size; 112 | 113 | if ($scale = next($precision)) { 114 | $attributes['scale'] = (int) $scale; 115 | } 116 | } 117 | 118 | /** 119 | * @param \Illuminate\Support\Fluent $attributes 120 | */ 121 | protected function parseName(Fluent $attributes) 122 | { 123 | $attributes['name'] = $this->get('column_name'); 124 | } 125 | 126 | /** 127 | * @param \Illuminate\Support\Fluent $attributes 128 | * @todo 129 | */ 130 | protected function parseAutoincrement(Fluent $attributes) 131 | { 132 | $attributes['autoincrement'] = preg_match('/serial/i', 133 | $this->get('data_type', '')) || $this->defaultIsNextVal($attributes); 134 | } 135 | 136 | /** 137 | * @param \Illuminate\Support\Fluent $attributes 138 | */ 139 | protected function parseNullable(Fluent $attributes) 140 | { 141 | $attributes['nullable'] = $this->same('is_nullable', 'YES'); 142 | } 143 | 144 | /** 145 | * @param \Illuminate\Support\Fluent $attributes 146 | */ 147 | protected function parseDefault(Fluent $attributes) 148 | { 149 | $value = null; 150 | if ($this->defaultIsNextVal($attributes)) { 151 | $attributes['autoincrement'] = true; 152 | } else { 153 | $value = $this->get('column_default', $this->get('generation_expression', null)); 154 | } 155 | $attributes['default'] = $value; 156 | } 157 | 158 | /** 159 | * @param \Illuminate\Support\Fluent $attributes 160 | * @todo 161 | */ 162 | protected function parseComment(Fluent $attributes) 163 | { 164 | $attributes['comment'] = $this->get('Comment'); 165 | } 166 | 167 | /** 168 | * @param string $key 169 | * @param mixed $default 170 | * 171 | * @return mixed 172 | */ 173 | protected function get($key, $default = null) 174 | { 175 | return Arr::get($this->metadata, $key, $default); 176 | } 177 | 178 | /** 179 | * @param string $key 180 | * @param string $value 181 | * 182 | * @return bool 183 | */ 184 | protected function same($key, $value) 185 | { 186 | return strcasecmp($this->get($key, ''), $value) === 0; 187 | } 188 | 189 | /** 190 | * @param \Illuminate\Support\Fluent $attributes 191 | * 192 | * @return bool 193 | */ 194 | private function defaultIsNextVal(Fluent $attributes) 195 | { 196 | $value = $this->get('column_default', $this->get('generation_expression', null)); 197 | $isIdentity = $this->get('is_identity'); 198 | $identityGeneration = $this->get('identity_generation'); 199 | 200 | return preg_match('/nextval\(/i', $value) || ($isIdentity === 'YES' && $identityGeneration === 'BY DEFAULT'); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Meta/Postgres/Schema.php: -------------------------------------------------------------------------------- 1 | schema_database = Config::get("database.connections.pgsql.schema"); 51 | if (!$this->schema_database){ 52 | $this->schema_database = 'public'; 53 | } 54 | $this->schema = $schema; 55 | $this->connection = $connection; 56 | 57 | $this->load(); 58 | } 59 | 60 | /** 61 | * @return \Doctrine\DBAL\Schema\AbstractSchemaManager 62 | * @todo: Use Doctrine instead of raw database queries 63 | */ 64 | public function manager() 65 | { 66 | return $this->connection->getDoctrineSchemaManager(); 67 | } 68 | 69 | /** 70 | * Loads schema's tables' information from the database. 71 | */ 72 | protected function load() 73 | { 74 | // Note that "schema" refers to the database name, 75 | // not a pgsql schema. 76 | $this->connection->raw('\c '.$this->wrap($this->schema)); 77 | $tables = $this->fetchTables($this->schema); 78 | foreach ($tables as $table) { 79 | $blueprint = new Blueprint($this->connection->getName(), $this->schema, $table); 80 | $this->fillColumns($blueprint); 81 | $this->fillConstraints($blueprint); 82 | $this->tables[$table] = $blueprint; 83 | } 84 | $this->loaded = true; 85 | } 86 | 87 | /** 88 | * @param string $schema 89 | * 90 | * @return array 91 | */ 92 | protected function fetchTables() 93 | { 94 | $rows = $this->arraify($this->connection->select( 95 | "SELECT * FROM pg_tables where schemaname='$this->schema_database'" 96 | )); 97 | $names = array_column($rows, 'tablename'); 98 | 99 | return Arr::flatten($names); 100 | } 101 | 102 | /** 103 | * @param \Reliese\Meta\Blueprint $blueprint 104 | */ 105 | protected function fillColumns(Blueprint $blueprint) 106 | { 107 | $rows = $this->arraify($this->connection->select( 108 | 'SELECT * FROM information_schema.columns '. 109 | "WHERE table_schema='$this->schema_database'". 110 | 'AND table_name='.$this->wrap($blueprint->table()) 111 | )); 112 | foreach ($rows as $column) { 113 | $blueprint->withColumn( 114 | $this->parseColumn($column) 115 | ); 116 | } 117 | } 118 | 119 | /** 120 | * @param array $metadata 121 | * 122 | * @return \Illuminate\Support\Fluent 123 | */ 124 | protected function parseColumn($metadata) 125 | { 126 | return (new Column($metadata))->normalize(); 127 | } 128 | 129 | /** 130 | * @param \Reliese\Meta\Blueprint $blueprint 131 | */ 132 | protected function fillConstraints(Blueprint $blueprint) 133 | { 134 | $sql = ' 135 | SELECT child.attname, p.contype, p.conname, 136 | parent_class.relname as parent_table, 137 | parent.attname as parent_attname 138 | FROM pg_attribute child 139 | JOIN pg_class child_class ON child_class.oid = child.attrelid 140 | LEFT JOIN pg_constraint p ON p.conrelid = child_class.oid 141 | AND child.attnum = ANY (p.conkey) 142 | LEFT JOIN pg_attribute parent on parent.attnum = ANY (p.confkey) 143 | AND parent.attrelid = p.confrelid 144 | LEFT JOIN pg_class parent_class on parent_class.oid = p.confrelid 145 | WHERE child_class.relkind = \'r\'::char 146 | AND child_class.relname = \''.$blueprint->table().'\' 147 | AND child.attnum > 0 148 | AND contype IS NOT NULL 149 | ORDER BY child.attnum 150 | ;'; 151 | $relations = $this->arraify($this->connection->select($sql)); 152 | 153 | $this->fillPrimaryKey($relations, $blueprint); 154 | $this->fillRelations($relations, $blueprint); 155 | 156 | $sql = 'SELECT * FROM pg_indexes WHERE tablename = \''.$blueprint->table().'\';'; 157 | $indexes = $this->arraify($this->connection->select($sql)); 158 | $this->fillIndexes($indexes, $blueprint); 159 | } 160 | 161 | /** 162 | * Quick little hack since it is no longer possible to set PDO's fetch mode 163 | * to PDO::FETCH_ASSOC. 164 | * 165 | * @param $data 166 | * @return mixed 167 | */ 168 | protected function arraify($data) 169 | { 170 | return json_decode(json_encode($data), true); 171 | } 172 | 173 | /** 174 | * @param array $relations 175 | * @param \Reliese\Meta\Blueprint $blueprint 176 | * @todo: Support named primary keys 177 | */ 178 | protected function fillPrimaryKey($relations, Blueprint $blueprint) 179 | { 180 | $pk = []; 181 | foreach ($relations as $row) { 182 | if ($row['contype'] === 'p') { 183 | $pk[] = $row['attname']; 184 | } 185 | } 186 | 187 | $key = [ 188 | 'name' => 'primary', 189 | 'index' => '', 190 | 'columns' => $pk, 191 | ]; 192 | 193 | $blueprint->withPrimaryKey(new Fluent($key)); 194 | } 195 | 196 | /** 197 | * @param array $indexes 198 | * @param \Reliese\Meta\Blueprint $blueprintx 199 | */ 200 | protected function fillIndexes($indexes, Blueprint $blueprint) 201 | { 202 | foreach ($indexes as $row) { 203 | $pattern = '/\s*(UNIQUE)?\s*(KEY|INDEX)\s+(\w+)\s+\(([^\)]+)\)/mi'; 204 | if (preg_match($pattern, $row['indexdef'], $setup) == false) { 205 | continue; 206 | } 207 | 208 | $index = [ 209 | 'name' => strcasecmp($setup[1], 'unique') === 0 ? 'unique' : 'index', 210 | 'columns' => $this->columnize($setup[4]), 211 | 'index' => $setup[3], 212 | ]; 213 | $blueprint->withIndex(new Fluent($index)); 214 | } 215 | } 216 | 217 | /** 218 | * @param array $relations 219 | * @param \Reliese\Meta\Blueprint $blueprint 220 | * @todo: Support named foreign keys 221 | */ 222 | protected function fillRelations($relations, Blueprint $blueprint) 223 | { 224 | $fk = []; 225 | foreach ($relations as $row) { 226 | $relName = $row['conname']; 227 | if ($row['contype'] === 'f') { 228 | if (! array_key_exists($relName, $fk)) { 229 | $fk[$relName] = [ 230 | 'columns' => [], 231 | 'ref' => [], 232 | ]; 233 | } 234 | $fk[$relName]['columns'][] = $row['attname']; 235 | $fk[$relName]['ref'][] = $row['parent_attname']; 236 | $fk[$relName]['table'] = $row['parent_table']; 237 | } 238 | } 239 | 240 | foreach ($fk as $row) { 241 | $relation = [ 242 | 'name' => 'foreign', 243 | 'index' => '', 244 | 'columns' => $row['columns'], 245 | 'references' => $row['ref'], 246 | 'on' => [$this->schema, $row['table']], 247 | ]; 248 | 249 | $blueprint->withRelation(new Fluent($relation)); 250 | } 251 | } 252 | 253 | /** 254 | * @param string $columns 255 | * 256 | * @return array 257 | */ 258 | protected function columnize($columns) 259 | { 260 | return array_map('trim', explode(',', $columns)); 261 | } 262 | 263 | /** 264 | * Wrap within backticks. 265 | * 266 | * @param string $table 267 | * 268 | * @return string 269 | */ 270 | protected function wrap($table) 271 | { 272 | $pieces = explode('.', str_replace('\'', '', $table)); 273 | 274 | return implode('.', array_map(function ($piece) { 275 | return "'$piece'"; 276 | }, $pieces)); 277 | } 278 | 279 | /** 280 | * @param \Illuminate\Database\Connection $connection 281 | * 282 | * @return array 283 | */ 284 | public static function schemas(Connection $connection) 285 | { 286 | $schemas = $connection->select('SELECT datname FROM pg_database'); 287 | $schemas = array_column($schemas, 'datname'); 288 | 289 | return array_diff($schemas, [ 290 | 'postgres', 291 | 'template0', 292 | 'template1', 293 | ]); 294 | } 295 | 296 | /** 297 | * @return string 298 | */ 299 | public function schema() 300 | { 301 | return $this->schema; 302 | } 303 | 304 | /** 305 | * @param string $table 306 | * 307 | * @return bool 308 | */ 309 | public function has($table) 310 | { 311 | return array_key_exists($table, $this->tables); 312 | } 313 | 314 | /** 315 | * @return \Reliese\Meta\Blueprint[] 316 | */ 317 | public function tables() 318 | { 319 | return $this->tables; 320 | } 321 | 322 | /** 323 | * @param string $table 324 | * 325 | * @return \Reliese\Meta\Blueprint 326 | */ 327 | public function table($table) 328 | { 329 | if (! $this->has($table)) { 330 | throw new \InvalidArgumentException("Table [$table] does not belong to schema [{$this->schema}]"); 331 | } 332 | 333 | return $this->tables[$table]; 334 | } 335 | 336 | /** 337 | * @return \Illuminate\Database\MySqlConnection 338 | */ 339 | public function connection() 340 | { 341 | return $this->connection; 342 | } 343 | 344 | /** 345 | * @param \Reliese\Meta\Blueprint $table 346 | * 347 | * @return array 348 | */ 349 | public function referencing(Blueprint $table) 350 | { 351 | $references = []; 352 | 353 | foreach ($this->tables as $blueprint) { 354 | foreach ($blueprint->references($table) as $reference) { 355 | $references[] = [ 356 | 'blueprint' => $blueprint, 357 | 'reference' => $reference, 358 | ]; 359 | } 360 | } 361 | 362 | return $references; 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /src/Meta/Schema.php: -------------------------------------------------------------------------------- 1 | MySqlSchema::class, 29 | MariaDbConnection::class => MySqlSchema::class, 30 | SQLiteConnection::class => SqliteSchema::class, 31 | PostgresConnection::class => PostgresSchema::class, 32 | \Larapack\DoctrineSupport\Connections\MySqlConnection::class => MySqlSchema::class, 33 | \Staudenmeir\LaravelCte\Connections\MySqlConnection::class => MySqlSchema::class, 34 | ]; 35 | 36 | /** 37 | * @var \Illuminate\Database\ConnectionInterface 38 | */ 39 | private $connection; 40 | 41 | /** 42 | * @var \Reliese\Meta\Schema[] 43 | */ 44 | protected $schemas = []; 45 | 46 | /** 47 | * SchemaManager constructor. 48 | * 49 | * @param \Illuminate\Database\ConnectionInterface $connection 50 | */ 51 | public function __construct(ConnectionInterface $connection) 52 | { 53 | $this->connection = $connection; 54 | 55 | $this->boot(); 56 | } 57 | 58 | /** 59 | * Load all schemas from this connection. 60 | */ 61 | public function boot() 62 | { 63 | if (! $this->hasMapping()) { 64 | throw new RuntimeException("There is no Schema Mapper registered for [{$this->type()}] connection."); 65 | } 66 | 67 | $schemas = forward_static_call([$this->getMapper(), 'schemas'], $this->connection); 68 | 69 | foreach ($schemas as $schema) { 70 | $this->make($schema); 71 | } 72 | } 73 | 74 | /** 75 | * @param string $schema 76 | * 77 | * @return \Reliese\Meta\Schema 78 | */ 79 | public function make($schema) 80 | { 81 | if (array_key_exists($schema, $this->schemas)) { 82 | return $this->schemas[$schema]; 83 | } 84 | 85 | return $this->schemas[$schema] = $this->makeMapper($schema); 86 | } 87 | 88 | /** 89 | * @param string $schema 90 | * 91 | * @return \Reliese\Meta\Schema 92 | */ 93 | protected function makeMapper($schema) 94 | { 95 | $mapper = $this->getMapper(); 96 | 97 | return new $mapper($schema, $this->connection); 98 | } 99 | 100 | /** 101 | * @return string 102 | */ 103 | protected function getMapper() 104 | { 105 | return static::$lookup[$this->type()]; 106 | } 107 | 108 | /** 109 | * @return string 110 | */ 111 | protected function type() 112 | { 113 | return get_class($this->connection); 114 | } 115 | 116 | /** 117 | * @return bool 118 | */ 119 | protected function hasMapping() 120 | { 121 | return array_key_exists($this->type(), static::$lookup); 122 | } 123 | 124 | /** 125 | * Register a new connection mapper. 126 | * 127 | * @param string $connection 128 | * @param string $mapper 129 | */ 130 | public static function register($connection, $mapper) 131 | { 132 | static::$lookup[$connection] = $mapper; 133 | } 134 | 135 | /** 136 | * Get Iterator for schemas. 137 | * 138 | * @return \ArrayIterator 139 | */ 140 | public function getIterator() 141 | { 142 | return new ArrayIterator($this->schemas); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Meta/Sqlite/Column.php: -------------------------------------------------------------------------------- 1 | ['varchar', 'text', 'string', 'char', 'enum', 'tinytext', 'mediumtext', 'longtext'], 31 | 'datetime' => ['datetime', 'year', 'date', 'time', 'timestamp'], 32 | 'int' => ['bigint', 'int', 'integer', 'tinyint', 'smallint', 'mediumint'], 33 | 'float' => ['float', 'decimal', 'numeric', 'dec', 'fixed', 'double', 'real', 'double precision'], 34 | 'boolean' => ['longblob', 'blob', 'bit'], 35 | ]; 36 | 37 | /** 38 | * MysqlColumn constructor. 39 | * 40 | * @param array $metadata 41 | */ 42 | public function __construct($metadata = []) 43 | { 44 | $this->metadata = $metadata; 45 | } 46 | 47 | /** 48 | * @return \Illuminate\Support\Fluent 49 | */ 50 | public function normalize() 51 | { 52 | $attributes = new Fluent(); 53 | 54 | foreach ($this->metas as $meta) { 55 | $this->{'parse'.ucfirst($meta)}($attributes); 56 | } 57 | 58 | return $attributes; 59 | } 60 | 61 | /** 62 | * @param \Illuminate\Support\Fluent $attributes 63 | */ 64 | protected function parseType(Fluent $attributes) 65 | { 66 | $dataType = $this->metadata->getType()->getName(); 67 | 68 | foreach (static::$mappings as $phpType => $database) { 69 | if (in_array($dataType, $database)) { 70 | $attributes['type'] = $phpType; 71 | } 72 | } 73 | 74 | if ($attributes['type'] == 'int') { 75 | $attributes['unsigned'] = $this->metadata->getUnsigned(); 76 | } 77 | } 78 | 79 | /** 80 | * @param \Illuminate\Support\Fluent $attributes 81 | */ 82 | protected function parseName(Fluent $attributes) 83 | { 84 | $attributes['name'] = $this->metadata->getName(); 85 | } 86 | 87 | /** 88 | * @param \Illuminate\Support\Fluent $attributes 89 | */ 90 | protected function parseAutoincrement(Fluent $attributes) 91 | { 92 | $attributes['autoincrement'] = $this->metadata->getAutoincrement(); 93 | } 94 | 95 | /** 96 | * @param \Illuminate\Support\Fluent $attributes 97 | */ 98 | protected function parseNullable(Fluent $attributes) 99 | { 100 | $attributes['nullable'] = $this->metadata->getNotnull(); 101 | } 102 | 103 | /** 104 | * @param \Illuminate\Support\Fluent $attributes 105 | */ 106 | protected function parseDefault(Fluent $attributes) 107 | { 108 | $attributes['default'] = $this->metadata->getDefault(); 109 | } 110 | 111 | /** 112 | * @param \Illuminate\Support\Fluent $attributes 113 | */ 114 | protected function parseComment(Fluent $attributes) 115 | { 116 | $attributes['comment'] = $this->metadata->getComment(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Meta/Sqlite/Schema.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 44 | $this->connection = $connection; 45 | /* Sqlite has a bool type that doctrine isn't registering */ 46 | $this->connection->getDoctrineConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('bool', 'boolean'); 47 | $this->load(); 48 | } 49 | 50 | /** 51 | * @return \Doctrine\DBAL\Schema\AbstractSchemaManager 52 | * @todo: Use Doctrine instead of raw database queries 53 | */ 54 | public function manager() 55 | { 56 | return $this->connection->getDoctrineSchemaManager(); 57 | } 58 | 59 | /** 60 | * Loads schema's tables' information from the database. 61 | */ 62 | protected function load() 63 | { 64 | $tables = $this->fetchTables(); 65 | 66 | foreach ($tables as $table) { 67 | $blueprint = new Blueprint($this->connection->getName(), $this->schema, $table); 68 | $this->fillColumns($blueprint); 69 | $this->fillConstraints($blueprint); 70 | $this->tables[$table] = $blueprint; 71 | } 72 | } 73 | 74 | /** 75 | * @return array 76 | * @internal param string $schema 77 | */ 78 | protected function fetchTables() 79 | { 80 | $names = $this->manager()->listTableNames(); 81 | 82 | return array_diff($names, [ 83 | 'sqlite_master', 84 | 'sqlite_sequence', 85 | 'sqlite_stat1', 86 | ]); 87 | } 88 | 89 | /** 90 | * @param \Reliese\Meta\Blueprint $blueprint 91 | */ 92 | protected function fillColumns(Blueprint $blueprint) 93 | { 94 | $columns = $this->manager()->listTableColumns($blueprint->table()); 95 | 96 | foreach ($columns as $column) { 97 | $blueprint->withColumn( 98 | $this->parseColumn($column) 99 | ); 100 | } 101 | } 102 | 103 | /** 104 | * @param \Doctrine\DBAL\Schema\Column $metadata 105 | * 106 | * @return \Illuminate\Support\Fluent 107 | */ 108 | protected function parseColumn($metadata) 109 | { 110 | return (new Column($metadata))->normalize(); 111 | } 112 | 113 | /** 114 | * @param \Reliese\Meta\Blueprint $blueprint 115 | */ 116 | protected function fillConstraints(Blueprint $blueprint) 117 | { 118 | $this->fillPrimaryKey($blueprint); 119 | $this->fillIndexes($blueprint); 120 | 121 | $this->fillRelations($blueprint); 122 | } 123 | 124 | /** 125 | * Quick little hack since it is no longer possible to set PDO's fetch mode 126 | * to PDO::FETCH_ASSOC. 127 | * 128 | * @param $data 129 | * @return mixed 130 | */ 131 | protected function arraify($data) 132 | { 133 | return json_decode(json_encode($data), true); 134 | } 135 | 136 | /** 137 | * @param \Reliese\Meta\Blueprint $blueprint 138 | * @todo: Support named primary keys 139 | */ 140 | protected function fillPrimaryKey(Blueprint $blueprint) 141 | { 142 | $indexes = $this->manager()->listTableIndexes($blueprint->table()); 143 | 144 | $key = [ 145 | 'name' => 'primary', 146 | 'index' => '', 147 | 'columns' => optional($indexes['primary']??null)->getColumns()?:[], 148 | ]; 149 | 150 | $blueprint->withPrimaryKey(new Fluent($key)); 151 | } 152 | 153 | /** 154 | * @param \Reliese\Meta\Blueprint $blueprint 155 | * @internal param string $sql 156 | */ 157 | protected function fillIndexes(Blueprint $blueprint) 158 | { 159 | $indexes = $this->manager()->listTableIndexes($blueprint->table()); 160 | unset($indexes['primary']); 161 | 162 | foreach ($indexes as $setup) { 163 | $index = [ 164 | 'name' => $setup->isUnique() ? 'unique' : 'index', 165 | 'columns' => $setup->getColumns(), 166 | 'index' => $setup->getName(), 167 | ]; 168 | $blueprint->withIndex(new Fluent($index)); 169 | } 170 | } 171 | 172 | /** 173 | * @param \Reliese\Meta\Blueprint $blueprint 174 | * @todo: Support named foreign keys 175 | */ 176 | protected function fillRelations(Blueprint $blueprint) 177 | { 178 | $relations = $this->manager()->listTableForeignKeys($blueprint->table()); 179 | 180 | foreach ($relations as $setup) { 181 | $table = ['database' => '', 'table'=>$setup->getForeignTableName()]; 182 | 183 | $relation = [ 184 | 'name' => 'foreign', 185 | 'index' => '', 186 | 'columns' => $setup->getColumns(), 187 | 'references' => $setup->getForeignColumns(), 188 | 'on' => $table, 189 | ]; 190 | 191 | $blueprint->withRelation(new Fluent($relation)); 192 | } 193 | } 194 | 195 | /** 196 | * @param \Illuminate\Database\Connection $connection 197 | * 198 | * @return array 199 | */ 200 | public static function schemas(Connection $connection) 201 | { 202 | return ['database']; 203 | } 204 | 205 | /** 206 | * @return string 207 | */ 208 | public function schema() 209 | { 210 | return $this->schema; 211 | } 212 | 213 | /** 214 | * @param string $table 215 | * 216 | * @return bool 217 | */ 218 | public function has($table) 219 | { 220 | return array_key_exists($table, $this->tables); 221 | } 222 | 223 | /** 224 | * @return \Reliese\Meta\Blueprint[] 225 | */ 226 | public function tables() 227 | { 228 | return $this->tables; 229 | } 230 | 231 | /** 232 | * @param string $table 233 | * 234 | * @return \Reliese\Meta\Blueprint 235 | */ 236 | public function table($table) 237 | { 238 | if (! $this->has($table)) { 239 | throw new \InvalidArgumentException("Table [$table] does not belong to schema [{$this->schema}]"); 240 | } 241 | 242 | return $this->tables[$table]; 243 | } 244 | 245 | /** 246 | * @return \Illuminate\Database\MySqlConnection 247 | */ 248 | public function connection() 249 | { 250 | return $this->connection; 251 | } 252 | 253 | /** 254 | * @param \Reliese\Meta\Blueprint $table 255 | * 256 | * @return array 257 | */ 258 | public function referencing(Blueprint $table) 259 | { 260 | $references = []; 261 | 262 | foreach ($this->tables as $blueprint) { 263 | foreach ($blueprint->references($table) as $reference) { 264 | $references[] = [ 265 | 'blueprint' => $blueprint, 266 | 'reference' => $reference, 267 | ]; 268 | } 269 | } 270 | 271 | return $references; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/Support/Classify.php: -------------------------------------------------------------------------------- 1 | ".static::export($value, $tabs + 1); 45 | }, $value, $keys); 46 | 47 | return "[\n$indent".implode(",\n$indent", $array)."\n$closingIndent]"; 48 | } 49 | 50 | // Default variable exporting 51 | return static::hasStaticCall($value) ? $value : var_export($value, true); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Coders/Console/Model/ModelTest.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'castType' => 'int', 16 | 'nullable' => false, 17 | 'expect' => 'int', 18 | ], 19 | 'Nullable int' => [ 20 | 'castType' => 'int', 21 | 'nullable' => true, 22 | 'expect' => 'int|null', 23 | ], 24 | 'Non-nullable json' => [ 25 | 'castType' => 'json', 26 | 'nullable' => false, 27 | 'expect' => 'array', 28 | ], 29 | 'Nullable json' => [ 30 | 'castType' => 'json', 31 | 'nullable' => true, 32 | 'expect' => 'array|null', 33 | ], 34 | 'Non-nullable date' => [ 35 | 'castType' => 'date', 36 | 'nullable' => false, 37 | 'expect' => '\Carbon\Carbon', 38 | ], 39 | 'Nullable date' => [ 40 | 'castType' => 'date', 41 | 'nullable' => true, 42 | 'expect' => '\Carbon\Carbon|null', 43 | ], 44 | ]; 45 | } 46 | 47 | /** 48 | * @dataProvider dataForTestPhpTypeHint 49 | * 50 | * @param string $castType 51 | * @param bool $nullable 52 | * @param string $expect 53 | */ 54 | public function testPhpTypeHint($castType, $nullable, $expect) 55 | { 56 | $model = new Model( 57 | new Blueprint('test', 'test', 'test'), 58 | new Factory( 59 | \Mockery::mock(\Illuminate\Database\DatabaseManager::class), 60 | \Mockery::mock(Illuminate\Filesystem\Filesystem::class), 61 | \Mockery::mock(\Reliese\Support\Classify::class), 62 | new \Reliese\Coders\Model\Config() 63 | ) 64 | ); 65 | 66 | $result = $model->phpTypeHint($castType, $nullable); 67 | $this->assertSame($expect, $result); 68 | } 69 | 70 | /** 71 | * @dataProvider provideDataForTestNullableRelationships 72 | * @param bool $nullable 73 | * @param string $expectedTypehint 74 | */ 75 | public function testBelongsToNullableRelationships($nullable, $expectedTypehint) 76 | { 77 | $columnDefinition = new Fluent( 78 | [ 79 | 'nullable' => $nullable, 80 | ] 81 | ); 82 | 83 | $baseBlueprint = Mockery::mock(Blueprint::class); 84 | $baseBlueprint->shouldReceive('columns')->andReturn([$columnDefinition]); 85 | $baseBlueprint->shouldReceive('schema')->andReturn('test'); 86 | $baseBlueprint->shouldReceive('qualifiedTable')->andReturn('test.test'); 87 | $baseBlueprint->shouldReceive('connection')->andReturn('test'); 88 | $baseBlueprint->shouldReceive('primaryKey')->andReturn(new Fluent(['columns' => []])); 89 | $baseBlueprint->shouldReceive('relations')->andReturn([]); 90 | $baseBlueprint->shouldReceive('table')->andReturn('things'); 91 | $baseBlueprint->shouldReceive('column')->andReturn($columnDefinition); 92 | 93 | $model = new Model( 94 | $baseBlueprint, 95 | new Factory( 96 | \Mockery::mock(\Illuminate\Database\DatabaseManager::class), 97 | \Mockery::mock(Illuminate\Filesystem\Filesystem::class), 98 | \Mockery::mock(\Reliese\Support\Classify::class), 99 | new \Reliese\Coders\Model\Config() 100 | ) 101 | ); 102 | 103 | $relation = new BelongsTo( 104 | new Fluent([ 105 | 'columns' => [ 106 | $columnDefinition 107 | ] 108 | ]), 109 | $model, 110 | $model 111 | ); 112 | 113 | $this->assertSame($expectedTypehint, $relation->hint()); 114 | } 115 | 116 | public function provideDataForTestNullableRelationships() 117 | { 118 | return [ 119 | 'Nullable Relation' => [ 120 | true, '\\\\Thing|null' 121 | ], 122 | 'Non Nullable Relation' => [ 123 | false, '\\\\Thing' 124 | ] 125 | ]; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/Coders/Model/ConfigTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('schema')->andReturn('test'); 22 | $baseBlueprint->shouldReceive('qualifiedTable')->andReturn('test.my_table'); 23 | $baseBlueprint->shouldReceive('connection')->andReturn('test_connection'); 24 | $baseBlueprint->shouldReceive('table')->andReturn('my_table'); 25 | 26 | $this->assertEquals($expected, $config->get($baseBlueprint, $key)); 27 | } 28 | 29 | public function provideDataForTestGet() 30 | { 31 | return [ 32 | 'Basic Key' => [ 33 | [ 34 | '*' => [ 35 | 'Key' => 'Value' 36 | ], 37 | ], 38 | 'Key', 39 | 'Value' 40 | ], 41 | 'Schema Key' => [ 42 | [ 43 | 'test' => [ 44 | 'schemaKey' => 'Schema Value' 45 | ], 46 | ], 47 | 'schemaKey', 48 | 'Schema Value' 49 | ], 50 | 'Qualified Table Key' => [ 51 | [ 52 | 'test' => [ 53 | 'qfKey' => 'Qualified Table Value' 54 | ], 55 | ], 56 | 'qfKey', 57 | 'Qualified Table Value' 58 | ], 59 | 'Connection Basic Key' => [ 60 | [ 61 | '@connections' => [ 62 | 'test_connection' => [ 63 | 'cKey' => 'Connection Value' 64 | ], 65 | ] 66 | ], 67 | 'cKey', 68 | 'Connection Value' 69 | ], 70 | 'Connection Schema Key' => [ 71 | [ 72 | '@connections' => [ 73 | 'test_connection' => [ 74 | 'test' => [ 75 | 'csKey' => 'Connection Schema Value' 76 | ] 77 | ], 78 | ] 79 | ], 80 | 'csKey', 81 | 'Connection Schema Value' 82 | ], 83 | 'Connection Table Key' => [ 84 | [ 85 | '@connections' => [ 86 | 'test_connection' => [ 87 | 'my_table' => [ 88 | 'ctKey' => 'Connection Table Value' 89 | ] 90 | ], 91 | ] 92 | ], 93 | 'ctKey', 94 | 'Connection Table Value' 95 | ], 96 | 'Test Hierarchy Override for Schema' => [ 97 | [ 98 | '*' => [ 99 | 'FirstKey' => 'Some Value' 100 | ], 101 | 'test' => [ 102 | 'FirstKey' => 'A Second Value' 103 | ] 104 | ], 105 | 'FirstKey', 106 | 'A Second Value' 107 | ], 108 | 'Test Hierarchy Override for Qualified Table' => [ 109 | [ 110 | '*' => [ 111 | 'FirstKey' => 'Some Value' 112 | ], 113 | 'test' => [ 114 | 'FirstKey' => 'A Second Value', 115 | 'my_table' => [ 116 | 'FirstKey' => 'A Third Value' 117 | ] 118 | ], 119 | ], 120 | 'FirstKey', 121 | 'A Third Value' 122 | ], 123 | 'Test Hierarchy Override for Connection Basic Key' => [ 124 | [ 125 | '*' => [ 126 | 'FirstKey' => 'Some Value' 127 | ], 128 | 'test' => [ 129 | 'FirstKey' => 'A Second Value', 130 | 'my_table' => [ 131 | 'FirstKey' => 'A Third Value' 132 | ] 133 | ], 134 | '@connections' => [ 135 | 'test_connection' => [ 136 | 'FirstKey' => 'A Fourth Value', 137 | ] 138 | ] 139 | ], 140 | 'FirstKey', 141 | 'A Fourth Value' 142 | ], 143 | 'Test Hierarchy Override for Connection Schema Key' => [ 144 | [ 145 | '*' => [ 146 | 'FirstKey' => 'Some Value' 147 | ], 148 | 'test' => [ 149 | 'FirstKey' => 'A Second Value', 150 | 'my_table' => [ 151 | 'FirstKey' => 'A Third Value' 152 | ] 153 | ], 154 | '@connections' => [ 155 | 'test_connection' => [ 156 | 'FirstKey' => 'A Fourth Value', 157 | 'test' => [ 158 | 'FirstKey' => 'A Fifth Value' 159 | ] 160 | ] 161 | ] 162 | ], 163 | 'FirstKey', 164 | 'A Fifth Value' 165 | ], 166 | 'Test Hierarchy Override for Connection Table Key' => [ 167 | [ 168 | '*' => [ 169 | 'FirstKey' => 'Some Value' 170 | ], 171 | 'test' => [ 172 | 'FirstKey' => 'A Second Value', 173 | 'my_table' => [ 174 | 'FirstKey' => 'A Third Value' 175 | ] 176 | ], 177 | '@connections' => [ 178 | 'test_connection' => [ 179 | 'FirstKey' => 'A Fourth Value', 180 | 'test' => [ 181 | 'FirstKey' => 'A Fifth Value', 182 | ], 183 | 'my_table' => [ 184 | 'FirstKey' => 'A Sixth Value', 185 | ] 186 | ] 187 | ] 188 | ], 189 | 'FirstKey', 190 | 'A Sixth Value' 191 | ], 192 | ]; 193 | } 194 | } -------------------------------------------------------------------------------- /tests/Coders/Model/Relations/BelongsToTest.php: -------------------------------------------------------------------------------- 1 | makePartial(); 38 | 39 | $relatedModel = Mockery::mock(Model::class)->makePartial(); 40 | 41 | $subject = Mockery::mock(Model::class)->makePartial(); 42 | $subject->shouldReceive('getRelationNameStrategy')->andReturn('foreign_key'); 43 | $subject->shouldReceive('usesSnakeAttributes')->andReturn($usesSnakeAttributes); 44 | 45 | /** @var BelongsTo|\Mockery\Mock $relationship */ 46 | $relationship = Mockery::mock(BelongsTo::class, [$relation, $subject, $relatedModel])->makePartial(); 47 | $relationship->shouldAllowMockingProtectedMethods(); 48 | $relationship->shouldReceive('otherKey')->andReturn($primaryKey); 49 | $relationship->shouldReceive('foreignKey')->andReturn($foreignKey); 50 | 51 | $this->assertEquals( 52 | $expected, 53 | $relationship->name(), 54 | json_encode(compact('usesSnakeAttributes', 'primaryKey', 'foreignKey')) 55 | ); 56 | } 57 | 58 | public function provideRelatedStrategyPermutations() 59 | { 60 | // usesSnakeAttributes, relatedClassName, expected 61 | return [ 62 | [false, 'LineManager', 'lineManager'], 63 | [true, 'LineManager', 'line_manager'], 64 | ]; 65 | } 66 | 67 | /** 68 | * @dataProvider provideRelatedStrategyPermutations 69 | * 70 | * @param bool $usesSnakeAttributes 71 | * @param string $relatedClassName 72 | * @param string $expected 73 | */ 74 | public function testNameUsingRelatedStrategy($usesSnakeAttributes, $relatedClassName, $expected) 75 | { 76 | $relation = Mockery::mock(Fluent::class)->makePartial(); 77 | 78 | $relatedModel = Mockery::mock(Model::class)->makePartial(); 79 | $relatedModel->shouldReceive('getClassName')->andReturn($relatedClassName); 80 | 81 | $subject = Mockery::mock(Model::class)->makePartial(); 82 | $subject->shouldReceive('getRelationNameStrategy')->andReturn('related'); 83 | $subject->shouldReceive('usesSnakeAttributes')->andReturn($usesSnakeAttributes); 84 | 85 | /** @var BelongsTo|\Mockery\Mock $relationship */ 86 | $relationship = Mockery::mock(BelongsTo::class, [$relation, $subject, $relatedModel])->makePartial(); 87 | 88 | $this->assertEquals( 89 | $expected, 90 | $relationship->name(), 91 | json_encode(compact('usesSnakeAttributes', 'relatedClassName')) 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Coders/Model/Relations/HasManyTest.php: -------------------------------------------------------------------------------- 1 | makePartial(); 49 | 50 | $relatedModel = Mockery::mock(Model::class)->makePartial(); 51 | $relatedModel->shouldReceive('getClassName')->andReturn($relationName); 52 | 53 | $subject = Mockery::mock(Model::class)->makePartial(); 54 | $subject->shouldReceive('getRelationNameStrategy')->andReturn('foreign_key'); 55 | $subject->shouldReceive('usesSnakeAttributes')->andReturn($usesSnakeAttributes); 56 | $subject->shouldReceive('getClassName')->andReturn($subjectName); 57 | 58 | /** @var BelongsTo|\Mockery\Mock $relationship */ 59 | $relationship = Mockery::mock(HasMany::class, [$relation, $subject, $relatedModel])->makePartial(); 60 | $relationship->shouldAllowMockingProtectedMethods(); 61 | $relationship->shouldReceive('localKey')->andReturn($primaryKey); 62 | $relationship->shouldReceive('foreignKey')->andReturn($foreignKey); 63 | 64 | $this->assertEquals( 65 | $expected, 66 | $relationship->name(), 67 | json_encode(compact('usesSnakeAttributes', 'subjectName', 'relationName', 'primaryKey', 'foreignKey')) 68 | ); 69 | } 70 | 71 | public function provideRelatedStrategyPermutations() 72 | { 73 | // usesSnakeAttributes, subjectName, relatedName, expected 74 | return [ 75 | [false, 'StaffMember', 'BlogPost', 'blogPosts'], 76 | [true, 'StaffMember', 'BlogPost', 'blog_posts'], 77 | // Same table reference 78 | [false, 'StaffMember', 'StaffMember', 'staffMembers'] 79 | ]; 80 | } 81 | 82 | /** 83 | * @dataProvider provideRelatedStrategyPermutations 84 | * 85 | * @param bool $usesSnakeAttributes 86 | * @param string $subjectName 87 | * @param string $relationName 88 | * @param string $expected 89 | */ 90 | public function testNameUsingRelatedStrategy($usesSnakeAttributes, $subjectName, $relationName, $expected) 91 | { 92 | $relation = Mockery::mock(Fluent::class)->makePartial(); 93 | 94 | $relatedModel = Mockery::mock(Model::class)->makePartial(); 95 | $relatedModel->shouldReceive('getClassName')->andReturn($relationName); 96 | 97 | $subject = Mockery::mock(Model::class)->makePartial(); 98 | $subject->shouldReceive('getClassName')->andReturn($subjectName); 99 | $subject->shouldReceive('getRelationNameStrategy')->andReturn('related'); 100 | $subject->shouldReceive('usesSnakeAttributes')->andReturn($usesSnakeAttributes); 101 | 102 | /** @var BelongsTo|\Mockery\Mock $relationship */ 103 | $relationship = Mockery::mock(HasMany::class, [$relation, $subject, $relatedModel])->makePartial(); 104 | 105 | $this->assertEquals( 106 | $expected, 107 | $relationship->name(), 108 | json_encode(compact('usesSnakeAttributes', 'subjectName', 'relationName')) 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/Coders/Model/Relations/RelationHelperTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 38 | $expected, 39 | RelationHelper::stripSuffixFromForeignKey($usesSnakeAttributes, $primaryKey, $foreignKey), 40 | json_encode(compact('usesSnakeAttributes', 'primaryKey', 'foreignKey')) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Meta/BlueprintTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('connection', $blueprint->connection()); 16 | $this->assertEquals('schema', $blueprint->schema()); 17 | $this->assertEquals('table', $blueprint->table()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 |