├── LICENSE ├── README.md ├── composer.json ├── config └── datamapper.php ├── src ├── Annotations │ ├── Annotation.php │ ├── AutoIncrement.php │ ├── AutoUuid.php │ ├── Column.php │ ├── Embeddable.php │ ├── Embedded.php │ ├── Entity.php │ ├── Id.php │ ├── Relation.php │ ├── SoftDeletes.php │ ├── Table.php │ ├── Timestamps.php │ ├── ValueObject.php │ ├── Versionable.php │ └── Versioned.php ├── Console │ ├── SchemaCommand.php │ ├── SchemaCreateCommand.php │ ├── SchemaDropCommand.php │ └── SchemaUpdateCommand.php ├── Contracts │ ├── AggregateRoot.php │ ├── Entity.php │ ├── Model.php │ ├── Proxy.php │ └── ValueObject.php ├── DatamapperServiceProvider.php ├── Eloquent │ ├── AutoUuid.php │ ├── Builder.php │ ├── Collection.php │ ├── Generator.php │ ├── GraphBuilder.php │ ├── GraphNode.php │ └── Model.php ├── EntityManager.php ├── Metadata │ ├── AnnotationLoader.php │ ├── ClassFinder.php │ ├── Definitions │ │ ├── Attribute.php │ │ ├── Column.php │ │ ├── Definition.php │ │ ├── EmbeddedClass.php │ │ ├── Entity.php │ │ ├── Relation.php │ │ └── Table.php │ ├── EntityScanner.php │ └── EntityValidator.php ├── Providers │ ├── BaseServiceProvider.php │ ├── CommandsServiceProvider.php │ ├── LumenServiceProvider.php │ └── MetadataServiceProvider.php ├── Schema │ └── Builder.php └── Support │ ├── AggregateRoot.php │ ├── Collection.php │ ├── DataTransferObject.php │ ├── Entity.php │ ├── Facades │ └── EntityManager.php │ ├── Model.php │ ├── Proxy.php │ ├── ProxyCollection.php │ ├── Traits │ ├── SoftDeletes.php │ ├── Timestamps.php │ ├── Versionable.php │ ├── VersionableSoftDeletes.php │ └── VersionableTimestamps.php │ ├── ValueObject.php │ └── helpers.php └── stubs ├── model-morph-extension.stub ├── model-relation.stub └── model.stub /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Markus Wetzel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Datamapper 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/proai/laravel-datamapper/v/stable)](https://packagist.org/packages/proai/laravel-datamapper) [![Total Downloads](https://poser.pugx.org/proai/laravel-datamapper/downloads)](https://packagist.org/packages/proai/laravel-datamapper) [![Latest Unstable Version](https://poser.pugx.org/proai/laravel-datamapper/v/unstable)](https://packagist.org/packages/proai/laravel-datamapper) [![License](https://poser.pugx.org/proai/laravel-datamapper/license)](https://packagist.org/packages/proai/laravel-datamapper) 4 | 5 | **Important: This package is unmaintained and never hit production stage. I decided that it is not worse to develop a datamapper package, because there is no real advantage over using the Laravel Eloquent orm. Nevertheless this package is [well documented](https://proai.github.io/laravel-datamapper) and basically all features should work out of the box. So if someone is interested in using the datamapper pattern with Laravel, this package is a good starting point.** 6 | 7 | An easy to use data mapper ORM for Laravel 5 that fits perfectly to the approach of Domain Driven Design (DDD). In general the Laravel Data Mapper is an extension to the Laravel Query Builder. You can build queries by using all of the query builder methods and in addition you can pass Plain Old PHP Objects (POPO's) to the builder and also return POPO's from the builder. 8 | 9 | ## Installation 10 | 11 | Laravel Data Mapper is distributed as a composer package. So you first have to add the package to your `composer.json` file: 12 | 13 | ``` 14 | "proai/laravel-datamapper": "~1.0@dev" 15 | ``` 16 | 17 | Then you have to run `composer update` to install the package. Once this is completed, you have to add the service provider to the providers array in `config/app.php`: 18 | 19 | ``` 20 | 'ProAI\Datamapper\DatamapperServiceProvider' 21 | ``` 22 | 23 | If you want to use a facade for the entity manager, you can create an alias in the aliases array of `config/app.php`: 24 | 25 | ``` 26 | 'EM' => 'ProAI\Datamapper\Support\Facades\EntityManager' 27 | ``` 28 | 29 | Run `php artisan vendor:publish` to publish this package configuration. Afterwards you can edit the file `config/datamapper.php`. 30 | 31 | ## Documentation 32 | 33 | See the full **[Documentation](https://proai.github.io/laravel-datamapper)** for more information. 34 | 35 | ## Usage 36 | 37 | ### Annotations 38 | 39 | We will map all classes to a database table by using annotations. Annotations are doc-comments that you add to a class. The annotations are quite similar to the Doctrine2 annotations. Here is a simple example of a `User` class: 40 | 41 | ```php 42 | em = $em; 95 | } 96 | 97 | ... 98 | 99 | } 100 | ``` 101 | 102 | The entity manager selects a table by passing the classname of an entity to the manager (e. g. `$em->entity('Acme\Models\User')`. The `entity` method then returns an object of the modified Laravel Query Builder, so you can chain all query builder methods after it (see examples). 103 | 104 | ##### Example #1: Get one or many User objects 105 | 106 | `$user = $em->entity('Acme\Models\User')->find($id);` (returns a User object) 107 | 108 | `$users = $em->entity('Acme\Models\User')->all();` (returns an ArrayCollection of User objects) 109 | 110 | ##### Example #2: Insert, update and delete a record 111 | 112 | `$em->insert($user);` 113 | 114 | `$em->update($user);` 115 | 116 | `$em->delete($user);` 117 | 118 | Hint: Relational objects are not inserted or updated. 119 | 120 | ##### Example #3: Eager Loading 121 | 122 | `$users = $em->class('Acme\Models\User')->with('comments')->get();` 123 | 124 | You can use the `with()` method the same way as you use it with Eloquent objects. Chained dot notations can be used (e. g. `->with('comments.likes')`). 125 | 126 | ### Entity Plugins 127 | 128 | ##### Timestamps 129 | 130 | If an entity has the `@ORM\Timestamps` annotation, `$timestamps` will be set to true in the mapped Eloquent model, so the created at and updated at timestamp will be updated automatically on insert and update. 131 | 132 | Note: This plugin requires a `$createdAt` property and a `$updatedAt` property. You can use the `ProAI\Datamapper\Support\Traits\Timestamps` trait for this. 133 | 134 | ##### SoftDeletes 135 | 136 | If an entity has the `@ORM\SoftDeletes` annotation, you can use the soft deleting methods from Eloquent, e. g.: 137 | 138 | `$users = $em->class('Entity\User')->withTrashed()->all();` 139 | 140 | Note: This plugin requires a `$deletedAt` property. You can use the `ProAI\Datamapper\Support\Traits\SoftDeletes` trait for this. 141 | 142 | ##### Versioning 143 | 144 | If an entity has the `@ORM\Versionable` annotation and you have added the `@ORM\Versioned` annotation to all versioned properties, you can use the versioning methods of the [Eloquent Versioning](https://github.com/proai/eloquent-versioning) package. So make sure you have installed this package. 145 | 146 | By default the query builder returns always the latest version. If you want a specific version or all versions, you can use the following: 147 | 148 | `$user = $em->class('Entity\User')->version(2)->find($id);` 149 | 150 | `$users = $em->class('Entity\User')->where('id',$id)->allVersions()->get();` 151 | 152 | ### Presenters 153 | 154 | This package can be extended by the Laravel Datamapper Presenter package. Check out the [Laravel Datamapper Presenter](https://github.com/ProAI/laravel-datamapper-presenter) readme for more information. 155 | 156 | ## Support 157 | 158 | Bugs and feature requests are tracked on [GitHub](https://github.com/proai/laravel-datamapper/issues). 159 | 160 | ## License 161 | 162 | This package is released under the [MIT License](LICENSE). 163 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proai/laravel-datamapper", 3 | "description": "An easy to use data mapper for Laravel 5 that fits perfectly to the approach of Domain Driven Design (DDD).", 4 | "keywords": ["laravel","orm","mapper","datamapper","query","querybuilder","entity", "repository"], 5 | "homepage": "http://github.com/proai/laravel-datamapper", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Markus J. Wetzel", 10 | "email": "markuswetzel@gmx.net" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.4.0", 15 | "illuminate/console": "5.*", 16 | "illuminate/filesystem": "5.*", 17 | "illuminate/database": "5.*", 18 | "doctrine/annotations": "~1.2", 19 | "doctrine/dbal": "~2.5" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "ProAI\\Datamapper\\": "src/" 24 | } 25 | }, 26 | "extra": { 27 | "branch-alias": { 28 | "dev-master": "1.0-dev" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /config/datamapper.php: -------------------------------------------------------------------------------- 1 | true, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Morph Class Abbreviations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | If this option is enabled, all morph classnames will be converted to a 24 | | short lower case abbreviation. For example 'Acme\User' will be shortened 25 | | to 'user'. 26 | | 27 | */ 28 | 29 | 'morphclass_abbreviations' => true, 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Models Namespace 34 | |-------------------------------------------------------------------------- 35 | | 36 | | If a models namespace is defined, only the entities and value objects in 37 | | the sub-namespace will be scanned by the schema commands. Also you only 38 | | have to name the sub-namespace in annotations (i. e. for the 39 | | relatedEntity parameter of the @Relation annotation). 40 | | 41 | */ 42 | 43 | 'models_namespace' => '', 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Auto Scan 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Automatically scan entity classes and update database on page load. This 51 | | Option is useful in development mode. 52 | | 53 | */ 54 | 55 | 'auto_scan' => env('APP_AUTO_SCAN', false), 56 | 57 | ]; -------------------------------------------------------------------------------- /src/Annotations/Annotation.php: -------------------------------------------------------------------------------- 1 | finder = $finder; 64 | $this->scanner = $scanner; 65 | $this->schema = $schema; 66 | $this->models = $models; 67 | $this->config = $config; 68 | } 69 | 70 | /** 71 | * Get classes by class argument or by app namespace. 72 | * 73 | * @return void 74 | */ 75 | protected function getClasses() 76 | { 77 | $class = $this->argument('class'); 78 | 79 | // set classes 80 | if ($class) { 81 | if (class_exists($class)) { 82 | $classes = [$class]; 83 | } elseif (class_exists($this->config['models_namespace'] . '\\' . $class)) { 84 | $classes = [$this->config['models_namespace'] . '\\' . $class]; 85 | } else { 86 | throw new UnexpectedValueException('Classname is not valid.'); 87 | } 88 | } else { 89 | $classes = $this->finder->getClassesFromNamespace($this->config['models_namespace']); 90 | } 91 | 92 | return $classes; 93 | } 94 | 95 | /** 96 | * Output SQL queries. 97 | * 98 | * @param array $statements SQL statements 99 | * @return void 100 | */ 101 | protected function outputQueries($statements) 102 | { 103 | $this->info(PHP_EOL . 'Outputting queries:'); 104 | if (empty($statements)) { 105 | $this->info("No queries found."); 106 | } else { 107 | $this->info(implode(';' . PHP_EOL, $statements)); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Console/SchemaCreateCommand.php: -------------------------------------------------------------------------------- 1 | info(PHP_EOL . ' 0% Initializing'); 34 | 35 | // get classes 36 | $classes = $this->getClasses($this->config['models_namespace']); 37 | 38 | $this->info(' 25% Building metadata'); 39 | 40 | // build metadata 41 | $metadata = $this->scanner->scan($classes, $this->config['namespace_tablenames'], $this->config['morphclass_abbreviations']); 42 | 43 | $this->info(' 50% Generating entity models'); 44 | 45 | // generate eloquent models 46 | $this->models->generate($metadata, true); 47 | 48 | $this->info(' 75% Building database schema'); 49 | 50 | // build schema 51 | $statements = $this->schema->create($metadata); 52 | 53 | $this->info(PHP_EOL . 'Schema created successfully!'); 54 | 55 | // register presenters 56 | if ($this->option('presenter')) { 57 | $this->call('presenter:register'); 58 | } 59 | 60 | // output SQL queries 61 | if ($this->option('dump-sql')) { 62 | $this->outputQueries($statements); 63 | } 64 | } 65 | 66 | /** 67 | * Get the console command arguments. 68 | * 69 | * @return array 70 | */ 71 | protected function getArguments() 72 | { 73 | return array( 74 | array('class', InputArgument::OPTIONAL, 'The classname for the migration'), 75 | ); 76 | } 77 | 78 | /** 79 | * Get the console command options. 80 | * 81 | * @return array 82 | */ 83 | protected function getOptions() 84 | { 85 | return array( 86 | array('dump-sql', null, InputOption::VALUE_NONE, 'Search for all eloquent models.'), 87 | array('presenter', null, InputOption::VALUE_NONE, 'Also register presenters with this command.'), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Console/SchemaDropCommand.php: -------------------------------------------------------------------------------- 1 | getClasses($this->config['models_namespace']); 35 | 36 | // build metadata 37 | $metadata = $this->scanner->scan($classes, $this->config['namespace_tablenames'], $this->config['morphclass_abbreviations']); 38 | 39 | // clean generated eloquent models 40 | $this->models->clean(); 41 | 42 | // build schema 43 | $statements = $this->schema->drop($metadata); 44 | 45 | $this->info('Schema dropped successfully!'); 46 | 47 | // output SQL queries 48 | if ($this->option('dump-sql')) { 49 | $this->outputQueries($statements); 50 | } 51 | } 52 | 53 | /** 54 | * Get the console command arguments. 55 | * 56 | * @return array 57 | */ 58 | protected function getArguments() 59 | { 60 | return array( 61 | array('class', InputArgument::OPTIONAL, 'The classname for the migration'), 62 | ); 63 | } 64 | 65 | /** 66 | * Get the console command options. 67 | * 68 | * @return array 69 | */ 70 | protected function getOptions() 71 | { 72 | return array( 73 | array('dump-sql', null, InputOption::VALUE_NONE, 'Search for all eloquent models.'), 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Console/SchemaUpdateCommand.php: -------------------------------------------------------------------------------- 1 | info(PHP_EOL . ' 0% Initializing'); 34 | 35 | // get classes 36 | $classes = $this->getClasses($this->config['models_namespace']); 37 | 38 | $this->info(' 25% Building metadata'); 39 | 40 | // build metadata 41 | $metadata = $this->scanner->scan($classes, $this->config['namespace_tablenames'], $this->config['morphclass_abbreviations']); 42 | 43 | $this->info(' 50% Generating entity models'); 44 | 45 | // generate eloquent models 46 | $this->models->generate($metadata, $this->option('save-mode')); 47 | 48 | $this->info(' 75% Building database schema'); 49 | 50 | // build schema 51 | $statements = $this->schema->update($metadata, $this->option('save-mode')); 52 | 53 | $this->info(PHP_EOL . 'Schema updated successfully!'); 54 | 55 | // register presenters 56 | if ($this->option('presenter')) { 57 | $this->call('presenter:register'); 58 | } 59 | 60 | // output SQL queries 61 | if ($this->option('dump-sql')) { 62 | $this->outputQueries($statements); 63 | } 64 | } 65 | 66 | /** 67 | * Get the console command arguments. 68 | * 69 | * @return array 70 | */ 71 | protected function getArguments() 72 | { 73 | return array( 74 | array('class', InputArgument::OPTIONAL, 'The classname for the migration'), 75 | ); 76 | } 77 | 78 | /** 79 | * Get the console command options. 80 | * 81 | * @return array 82 | */ 83 | protected function getOptions() 84 | { 85 | return array( 86 | array('dump-sql', null, InputOption::VALUE_NONE, 'Search for all eloquent models.'), 87 | array('save-mode', null, InputOption::VALUE_NONE, 'Doctrine DBAL save mode for updating.'), 88 | array('presenter', null, InputOption::VALUE_NONE, 'Also register presenters with this command.'), 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Contracts/AggregateRoot.php: -------------------------------------------------------------------------------- 1 | registerConfig(); 17 | 18 | parent::register(); 19 | } 20 | 21 | /** 22 | * Register the config. 23 | * 24 | * @return void 25 | */ 26 | protected function registerConfig() 27 | { 28 | $configPath = __DIR__ . '/../config/datamapper.php'; 29 | 30 | $this->mergeConfigFrom($configPath, 'datamapper'); 31 | 32 | $this->publishes([$configPath => config_path('datamapper.php')], 'config'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Eloquent/AutoUuid.php: -------------------------------------------------------------------------------- 1 | getAutoUuids() as $autoUuid) { 18 | if (empty($model->{$autoUuid})) { 19 | $model->setAttribute($autoUuid, $model->generateUuid()->getBytes()); 20 | } 21 | } 22 | }); 23 | } 24 | 25 | /** 26 | * Get a new UUID. 27 | * 28 | * @return \Ramsey\Uuid\Uuid 29 | */ 30 | public function generateUuid() 31 | { 32 | return Uuid::uuid4(); 33 | } 34 | 35 | /** 36 | * Get the auto generated uuid columns array. 37 | * 38 | * @return array 39 | */ 40 | public function getAutoUuids() 41 | { 42 | return $this->autoUuids; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Eloquent/Builder.php: -------------------------------------------------------------------------------- 1 | query = $query; 52 | 53 | $this->returnType = $returnType; 54 | } 55 | 56 | /** 57 | * Execute the query as a "select" statement. 58 | * 59 | * @param array $columns 60 | * @return \Illuminate\Database\Eloquent\Collection|static[] 61 | */ 62 | public function get($columns = array('*')) 63 | { 64 | $results = parent::get($columns); 65 | 66 | switch($this->returnType) { 67 | case Builder::RETURN_TYPE_DATAMAPPER: 68 | return $results->toDatamapperObject(); 69 | case Builder::RETURN_TYPE_ELOQUENT: 70 | return $results; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/Eloquent/Collection.php: -------------------------------------------------------------------------------- 1 | items as $name => $item) { 21 | $entities->put($name, $item->toDatamapperObject()); 22 | } 23 | 24 | return $entities; 25 | } 26 | 27 | /** 28 | * Convert models to data transfer objects. 29 | * 30 | * @param string $root 31 | * @param array $schema 32 | * @param array $transformations 33 | * @param string $path 34 | * @return \ProAI\Datamapper\Support\Collection 35 | */ 36 | public function toDataTransferObject(string $root, array $schema, array $transformations, $path='') 37 | { 38 | $entities = new DatamapperCollection; 39 | 40 | foreach ($this->items as $name => $item) { 41 | $entities->put($name, $item->toDataTransferObject($root, $schema, $transformations, $path)); 42 | } 43 | 44 | return $entities; 45 | } 46 | 47 | /** 48 | * Convert models to eloquent models. 49 | * 50 | * @param \ProAI\Datamapper\Support\Collection $entities 51 | * @param string $lastObjectId 52 | * @param \ProAI\Datamapper\Eloquent\Model $lastEloquentModel 53 | * @return \ProAI\Datamapper\Eloquent\Collection 54 | */ 55 | public static function newFromDatamapperObject($entities, $lastObjectId, $lastEloquentModel) 56 | { 57 | $eloquentModels = new static; 58 | 59 | foreach ($entities as $name => $item) { 60 | if (spl_object_hash($item) == $lastObjectId) { 61 | $model = $lastEloquentModel; 62 | } else { 63 | $model = Model::newFromDatamapperObject($item, $lastObjectId, $lastEloquentModel); 64 | } 65 | 66 | $eloquentModels->put($name, $model); 67 | } 68 | 69 | return $eloquentModels; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Eloquent/Generator.php: -------------------------------------------------------------------------------- 1 | files = $files; 40 | $this->path = $path; 41 | 42 | $this->stubs['model'] = $this->files->get(__DIR__ . '/../../stubs/model.stub'); 43 | $this->stubs['relation'] = $this->files->get(__DIR__ . '/../../stubs/model-relation.stub'); 44 | $this->stubs['morph_extension'] = $this->files->get(__DIR__ . '/../../stubs/model-morph-extension.stub'); 45 | } 46 | 47 | /** 48 | * Generate model from metadata. 49 | * 50 | * @param array $metadata 51 | * @param boolean $saveMode 52 | * @return void 53 | */ 54 | public function generate($metadata, $saveMode=false) 55 | { 56 | // clean or make (if not exists) model storage directory 57 | if (! $this->files->exists($this->path)) { 58 | $this->files->makeDirectory($this->path); 59 | } 60 | 61 | // clear existing models if save mode is off 62 | if (! $saveMode) { 63 | $this->clean(); 64 | } 65 | 66 | // create models 67 | foreach ($metadata as $entityMetadata) { 68 | $this->generateModel($entityMetadata); 69 | } 70 | 71 | // create .gitignore 72 | $this->files->put($this->path . '/.gitignore', '*' . PHP_EOL . '!.gitignore'); 73 | 74 | // create json file for metadata 75 | $contents = json_encode($metadata, JSON_PRETTY_PRINT); 76 | $this->files->put($this->path . '/entities.json', $contents); 77 | } 78 | 79 | /** 80 | * Clean model directory. 81 | * 82 | * @return void 83 | */ 84 | public function clean() 85 | { 86 | if ($this->files->exists($this->path)) { 87 | $this->files->cleanDirectory($this->path); 88 | } 89 | } 90 | 91 | /** 92 | * Generate model from metadata. 93 | * 94 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 95 | * @return void 96 | */ 97 | public function generateModel($entityMetadata) 98 | { 99 | $stub = $this->stubs['model']; 100 | 101 | // header 102 | $this->replaceNamespace(get_mapped_model_namespace(), $stub); 103 | $this->replaceClass(class_basename(get_mapped_model($entityMetadata['class'], false)), $stub); 104 | $this->replaceMappedClass($entityMetadata['class'], $stub); 105 | 106 | // traits 107 | $this->replaceTraits($entityMetadata, $stub); 108 | 109 | // table name 110 | $this->replaceTable($entityMetadata['table']['name'], $stub); 111 | 112 | // primary key 113 | $columnMetadata = $this->getPrimaryKeyColumn($entityMetadata); 114 | $this->replacePrimaryKey($columnMetadata['name'], $stub); 115 | $this->replaceIncrementing((! empty($columnMetadata['options']['autoIncrement'])), $stub); 116 | 117 | $this->replaceAutoUuids($entityMetadata, $stub); 118 | 119 | // timestamps 120 | $this->replaceTimestamps($entityMetadata['timestamps'], $stub); 121 | 122 | // misc 123 | $this->replaceTouches($entityMetadata['touches'], $stub); 124 | $this->replaceWith($entityMetadata['with'], $stub); 125 | $this->replaceVersioned($entityMetadata['versionTable'], $stub); 126 | $this->replaceMorphClass($entityMetadata['morphClass'], $stub); 127 | 128 | // mapping data 129 | $this->replaceMapping($entityMetadata, $stub); 130 | 131 | // relations 132 | $this->replaceRelations($entityMetadata['relations'], $stub); 133 | 134 | $this->files->put($this->path . '/' . get_mapped_model_hash($entityMetadata['class']), $stub); 135 | } 136 | 137 | /** 138 | * Get primary key and auto increment value. 139 | * 140 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 141 | * @return \ProAI\Datamapper\Metadata\Definitions\Column 142 | */ 143 | protected function getPrimaryKeyColumn($entityMetadata) 144 | { 145 | $primaryKey = 'id'; 146 | $incrementing = true; 147 | 148 | foreach ($entityMetadata['table']['columns'] as $column) { 149 | if ($column['primary']) { 150 | return $column; 151 | } 152 | } 153 | } 154 | 155 | /** 156 | * Replace the namespace for the given stub. 157 | * 158 | * @param string $name 159 | * @param string $stub 160 | * @return void 161 | */ 162 | protected function replaceNamespace($name, &$stub) 163 | { 164 | $stub = str_replace('{{namespace}}', $name, $stub); 165 | } 166 | 167 | /** 168 | * Replace the classname for the given stub. 169 | * 170 | * @param string $name 171 | * @param string $stub 172 | * @return void 173 | */ 174 | protected function replaceClass($name, &$stub) 175 | { 176 | $stub = str_replace('{{class}}', $name, $stub); 177 | } 178 | 179 | /** 180 | * Replace the classname for the given stub. 181 | * 182 | * @param string $name 183 | * @param string $stub 184 | * @return void 185 | */ 186 | protected function replaceMappedClass($name, &$stub) 187 | { 188 | $stub = str_replace('{{mappedClass}}', "'".$name."'", $stub); 189 | } 190 | 191 | /** 192 | * Replace traits. 193 | * 194 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 195 | * @param string $stub 196 | * @return void 197 | */ 198 | protected function replaceTraits($entityMetadata, &$stub) 199 | { 200 | $traits = []; 201 | 202 | // versionable 203 | if (! empty($entityMetadata['versionTable'])) { 204 | $traits['versionable'] = 'use \ProAI\Versioning\Versionable;'; 205 | } 206 | 207 | // softDeletes 208 | if ($entityMetadata['softDeletes']) { 209 | $traits['softDeletes'] = 'use \ProAI\Versioning\SoftDeletes;'; 210 | } 211 | 212 | // autoUuid 213 | if ($this->hasAutoUuidColumn($entityMetadata)) { 214 | $traits['autoUuid'] = 'use \ProAI\Datamapper\Eloquent\AutoUuid;'; 215 | } 216 | 217 | $separator = PHP_EOL . PHP_EOL . ' '; 218 | $stub = str_replace('{{traits}}', implode($separator, $traits) . $separator, $stub); 219 | } 220 | 221 | /** 222 | * Does this model have an auto uuid column? 223 | * 224 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 225 | * @return boolean 226 | */ 227 | protected function hasAutoUuidColumn($entityMetadata) 228 | { 229 | foreach ($entityMetadata['table']['columns'] as $column) { 230 | if (! empty($column['options']['autoUuid'])) { 231 | return true; 232 | } 233 | } 234 | 235 | return false; 236 | } 237 | 238 | /** 239 | * Replace softDeletes. 240 | * 241 | * @param boolean $option 242 | * @param string $stub 243 | * @return void 244 | */ 245 | protected function replaceSoftDeletes($option, &$stub) 246 | { 247 | $stub = str_replace('{{softDeletes}}', $option ? 'use \ProAI\Versioning\SoftDeletes;' . PHP_EOL . PHP_EOL . ' ' : '', $stub); 248 | } 249 | 250 | /** 251 | * Replace versionable. 252 | * 253 | * @param boolean $option 254 | * @param string $stub 255 | * @return void 256 | */ 257 | protected function replaceVersionable($versionTable, &$stub) 258 | { 259 | $option = (! empty($versionTable)) ? true : false; 260 | $stub = str_replace('{{versionable}}', (! empty($versionTable)) ? 'use \ProAI\Versioning\Versionable;' . PHP_EOL . PHP_EOL . ' ' : '', $stub); 261 | } 262 | 263 | /** 264 | * Replace table name. 265 | * 266 | * @param boolean $name 267 | * @param string $stub 268 | * @return void 269 | */ 270 | protected function replaceTable($name, &$stub) 271 | { 272 | $stub = str_replace('{{table}}', "'".$name."'", $stub); 273 | } 274 | 275 | /** 276 | * Replace primary key. 277 | * 278 | * @param string $name 279 | * @param string $stub 280 | * @return void 281 | */ 282 | protected function replacePrimaryKey($name, &$stub) 283 | { 284 | $stub = str_replace('{{primaryKey}}', "'".$name."'", $stub); 285 | } 286 | 287 | /** 288 | * Replace incrementing. 289 | * 290 | * @param boolean $option 291 | * @param string $stub 292 | * @return void 293 | */ 294 | protected function replaceIncrementing($option, &$stub) 295 | { 296 | $stub = str_replace('{{incrementing}}', $option ? 'true' : 'false', $stub); 297 | } 298 | 299 | /** 300 | * Replace autoUuids. 301 | * 302 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 303 | * @param string $stub 304 | * @return void 305 | */ 306 | protected function replaceAutoUuids($entityMetadata, &$stub) 307 | { 308 | $autoUuids = []; 309 | 310 | foreach ($entityMetadata['table']['columns'] as $column) { 311 | if (! empty($column['options']['autoUuid'])) { 312 | $autoUuids[] = $column['name']; 313 | } 314 | } 315 | 316 | $stub = str_replace('{{autoUuids}}', $this->getArrayAsText($autoUuids), $stub); 317 | } 318 | 319 | /** 320 | * Replace timestamps. 321 | * 322 | * @param boolean $option 323 | * @param string $stub 324 | * @return void 325 | */ 326 | protected function replaceTimestamps($option, &$stub) 327 | { 328 | $stub = str_replace('{{timestamps}}', $option ? 'true' : 'false', $stub); 329 | } 330 | 331 | /** 332 | * Replace touches. 333 | * 334 | * @param array $touches 335 | * @param string $stub 336 | * @return void 337 | */ 338 | protected function replaceTouches($touches, &$stub) 339 | { 340 | $stub = str_replace('{{touches}}', $this->getArrayAsText($touches), $stub); 341 | } 342 | 343 | /** 344 | * Replace with. 345 | * 346 | * @param array $with 347 | * @param string $stub 348 | * @return void 349 | */ 350 | protected function replaceWith($with, &$stub) 351 | { 352 | $stub = str_replace('{{with}}', $this->getArrayAsText($with), $stub); 353 | } 354 | 355 | /** 356 | * Replace versioned. 357 | * 358 | * @param mixed $versionTable 359 | * @param string $stub 360 | * @return void 361 | */ 362 | protected function replaceVersioned($versionTable, &$stub) 363 | { 364 | if (! $versionTable) { 365 | $stub = str_replace('{{versioned}}', $this->getArrayAsText([]), $stub); 366 | return; 367 | } 368 | 369 | $versioned = []; 370 | foreach ($versionTable['columns'] as $column) { 371 | if (! $column['primary'] || $column['name'] == 'version') { 372 | $versioned[] = $column['name']; 373 | } 374 | } 375 | $stub = str_replace('{{versioned}}', $this->getArrayAsText($versioned), $stub); 376 | } 377 | 378 | /** 379 | * Replace the morph classname for the given stub. 380 | * 381 | * @param string $name 382 | * @param string $stub 383 | * @return void 384 | */ 385 | protected function replaceMorphClass($name, &$stub) 386 | { 387 | $stub = str_replace('{{morphClass}}', "'".$name."'", $stub); 388 | } 389 | 390 | /** 391 | * Replace mapping. 392 | * 393 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 394 | * @param string $stub 395 | * @return void 396 | */ 397 | protected function replaceMapping($entityMetadata, &$stub) 398 | { 399 | $attributes = []; 400 | foreach ($entityMetadata['attributes'] as $attributeMetadata) { 401 | $attributes[$attributeMetadata['name']] = $attributeMetadata['columnName']; 402 | } 403 | 404 | $embeddeds = []; 405 | foreach ($entityMetadata['embeddeds'] as $embeddedMetadata) { 406 | $embedded = []; 407 | $embedded['class'] = $embeddedMetadata['class']; 408 | $embedded['columnPrefix'] = $embeddedMetadata['columnPrefix']; 409 | $embeddedAttributes = []; 410 | foreach ($embeddedMetadata['attributes'] as $attributeMetadata) { 411 | $embeddedAttributes[$attributeMetadata['name']] = $attributeMetadata['columnName']; 412 | } 413 | $embedded['attributes'] = $embeddedAttributes; 414 | $embeddeds[$embeddedMetadata['name']] = $embedded; 415 | } 416 | 417 | $relations = []; 418 | foreach ($entityMetadata['relations'] as $relationMetadata) { 419 | $relation = []; 420 | 421 | $relation['type'] = $relationMetadata['type']; 422 | if ($relation['type'] == 'belongsToMany' || $relation['type'] == 'morphToMany') { 423 | $relation['inverse'] = (! empty($relationMetadata['options']['inverse'])); 424 | } 425 | 426 | $relations[$relationMetadata['name']] = $relation; 427 | } 428 | 429 | $mapping = [ 430 | 'attributes' => $attributes, 431 | 'embeddeds' => $embeddeds, 432 | 'relations' => $relations, 433 | ]; 434 | 435 | $stub = str_replace('{{mapping}}', $this->getArrayAsText($mapping), $stub); 436 | } 437 | 438 | /** 439 | * Replace relations. 440 | * 441 | * @param array $relations 442 | * @param string $stub 443 | * @return void 444 | */ 445 | protected function replaceRelations($relations, &$stub) 446 | { 447 | $textRelations = []; 448 | 449 | foreach ($relations as $key => $relation) { 450 | $relationStub = $this->stubs['relation']; 451 | 452 | // generate options array 453 | $options = []; 454 | 455 | if ($relation['type'] != 'morphTo') { 456 | $options[] = "'" . get_mapped_model($relation['relatedEntity'], false)."'"; 457 | } 458 | 459 | foreach ($relation['options'] as $name => $option) { 460 | if ($option === null) { 461 | $options[] = 'null'; 462 | } elseif ($option === true) { 463 | $options[] = 'true'; 464 | } elseif ($option === false) { 465 | $options[] = 'false'; 466 | } else { 467 | if ($name == 'throughEntity') { 468 | $options[] = "'".get_mapped_model($option, false)."'"; 469 | } elseif ($name != 'morphableClasses') { 470 | $options[] = "'".$option."'"; 471 | } 472 | } 473 | } 474 | 475 | $options = implode(", ", $options); 476 | 477 | $relationStub = str_replace('{{name}}', $relation['name'], $relationStub); 478 | $relationStub = str_replace('{{options}}', $options, $relationStub); 479 | $relationStub = str_replace('{{ucfirst_type}}', ucfirst($relation['type']), $relationStub); 480 | $relationStub = str_replace('{{type}}', $relation['type'], $relationStub); 481 | 482 | $textRelations[] = $relationStub; 483 | 484 | if ($relation['type'] == 'morphTo' 485 | || ($relation['type'] == 'morphToMany' && ! $relation['options']['inverse'])) { 486 | $morphStub = $this->stubs['morph_extension']; 487 | 488 | $morphableClasses = []; 489 | foreach ($relation['options']['morphableClasses'] as $key => $name) { 490 | $morphableClasses[$key] = get_mapped_model($name, false); 491 | } 492 | 493 | $morphStub = str_replace('{{name}}', $relation['name'], $morphStub); 494 | $morphStub = str_replace('{{morphName}}', ucfirst($relation['options']['morphName']), $morphStub); 495 | $morphStub = str_replace('{{types}}', $this->getArrayAsText($morphableClasses, 2), $morphStub); 496 | 497 | $textRelations[] = $morphStub; 498 | } 499 | } 500 | 501 | $stub = str_replace('{{relations}}', implode(PHP_EOL . PHP_EOL, $textRelations), $stub); 502 | } 503 | 504 | /** 505 | * Get an array in text format. 506 | * 507 | * @param array $array 508 | * @return string 509 | */ 510 | protected function getArrayAsText($array, $intendBy=1) 511 | { 512 | $intention = ''; 513 | for ($i=0; $i<$intendBy; $i++) { 514 | $intention .= ' '; 515 | } 516 | 517 | $text = var_export($array, true); 518 | 519 | $text = preg_replace('/[ ]{2}/', ' ', $text); 520 | $text = preg_replace("/\=\>[ \n ]+array[ ]+\(/", '=> array(', $text); 521 | return $text = preg_replace("/\n/", "\n".$intention, $text); 522 | } 523 | } 524 | -------------------------------------------------------------------------------- /src/Eloquent/GraphBuilder.php: -------------------------------------------------------------------------------- 1 | eloquentQuery = $eloquentQuery; 53 | } 54 | 55 | /** 56 | * Execute the query as a "select" statement. 57 | * 58 | * @return array|null 59 | */ 60 | public function get() 61 | { 62 | return $this->getResults('get'); 63 | } 64 | 65 | /** 66 | * Execute the query and get the first result. 67 | * 68 | * @return array|null 69 | */ 70 | public function first() 71 | { 72 | return $this->getResults('first'); 73 | } 74 | 75 | /** 76 | * Prepare query for execution. 77 | * 78 | * @param string $method 79 | * @return array|null 80 | */ 81 | protected function getResults($method) 82 | { 83 | if ($this->schema) { 84 | // set root constraints 85 | if (isset($this->constraints[$this->root])) { 86 | $this->constraints[$this->root]($this->eloquentQuery); 87 | } 88 | 89 | // set eager load constraints 90 | $this->eloquentQuery->setEagerLoads($this->parseRelationsFromSchema($this->schema, '')); 91 | 92 | // execute query 93 | $results = $this->eloquentQuery->$method(); 94 | 95 | // transform to data transfer objects 96 | if ($results) { 97 | $dtos = $results->toDataTransferObject($this->root, $this->schema, $this->transformations); 98 | 99 | return [$this->root => $dtos]; 100 | } 101 | } 102 | 103 | return null; 104 | } 105 | 106 | /** 107 | * Parse relations from schema. 108 | * 109 | * @param array $schema 110 | * @param string $path 111 | * @return void 112 | */ 113 | protected function parseRelationsFromSchema(array $schema, $path='') 114 | { 115 | $results = []; 116 | 117 | foreach ($schema as $key => $value) { 118 | if (! is_numeric($key)) { 119 | // join relation 120 | if (substr($key, 0, 3) != '...') { 121 | $childPath = ($path) 122 | ? $path.'.'.$key 123 | : $key; 124 | $results[$childPath] = (isset($this->constraints[$this->root.'.'.$childPath])) 125 | ? $this->constraints[$this->root.'.'.$childPath] 126 | : function () {}; 127 | } else { 128 | $childPath = $path; 129 | } 130 | 131 | // recursive call 132 | $results = array_merge($results, $this->parseRelationsFromSchema($value, $childPath)); 133 | } 134 | } 135 | 136 | return $results; 137 | } 138 | 139 | /** 140 | * Set the schema. 141 | * 142 | * @param array $schema 143 | * @return $this 144 | */ 145 | public function schema(array $schema) 146 | { 147 | $this->root = key($schema); 148 | 149 | $this->schema = current($schema); 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * Constraints for query relations. 156 | * 157 | * @param array $constraints 158 | * @return $this 159 | */ 160 | public function constraints(array $constraints) 161 | { 162 | $this->constraints = array_merge($this->constraints, $constraints); 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * Transform model attributes to data transfer attributes 169 | * 170 | * @param array $transformations 171 | * @return $this 172 | */ 173 | public function transform($transformations) 174 | { 175 | $this->transformations = array_merge($this->transformations, $transformations); 176 | 177 | return $this; 178 | } 179 | } -------------------------------------------------------------------------------- /src/Eloquent/GraphNode.php: -------------------------------------------------------------------------------- 1 | value = $value; 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * Set description. 36 | * 37 | * @param string $value 38 | * @return void 39 | */ 40 | public function description($description) 41 | { 42 | $this->description = $description; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Get value. 49 | * 50 | * @return void 51 | */ 52 | public function getValue() 53 | { 54 | return $this->value; 55 | } 56 | 57 | /** 58 | * Get description. 59 | * 60 | * @return void 61 | */ 62 | public function getDescription() 63 | { 64 | return $this->description; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Eloquent/Model.php: -------------------------------------------------------------------------------- 1 | newQueryWithoutScopes($returnType); 88 | 89 | // laravel 5.1 90 | if (method_exists($this, 'applyGlobalScopes')) { 91 | return $this->applyGlobalScopes($builder); 92 | } 93 | 94 | // laravel 5.2 95 | foreach ($this->getGlobalScopes() as $identifier => $scope) { 96 | $builder->withGlobalScope($identifier, $scope); 97 | } 98 | 99 | return $builder; 100 | } 101 | 102 | /** 103 | * Get a new query builder that doesn't have any global scopes. 104 | * 105 | * @param string $returnType 106 | * @return \Illuminate\Database\Eloquent\Builder|static 107 | */ 108 | public function newQueryWithoutScopes($returnType=Builder::RETURN_TYPE_ELOQUENT) 109 | { 110 | $builder = $this->newEloquentBuilder( 111 | $this->newBaseQueryBuilder(), 112 | $returnType 113 | ); 114 | 115 | // Once we have the query builders, we will set the model instances so the 116 | // builder can easily access any information it may need from the model 117 | // while it is constructing and executing various queries against it. 118 | return $builder->setModel($this)->with($this->with); 119 | } 120 | 121 | /** 122 | * Create a new Eloquent query builder for the model. 123 | * 124 | * @param \Illuminate\Database\Query\Builder $query 125 | * @return \ProAI\Datamapper\Eloquent\Builder|static 126 | */ 127 | public function newEloquentBuilder($query, $returnType=Builder::RETURN_TYPE_ELOQUENT) 128 | { 129 | return new Builder($query, $returnType); 130 | } 131 | 132 | /** 133 | * Create a new Eloquent query builder for the model. 134 | * 135 | * @param \Illuminate\Database\Query\Builder $query 136 | * @return \ProAI\Datamapper\Eloquent\GraphBuilder|static 137 | */ 138 | public function newGraphQuery() 139 | { 140 | return new GraphBuilder($this->newQuery()); 141 | } 142 | 143 | /** 144 | * Convert model to entity object. 145 | * 146 | * @return object 147 | */ 148 | public function toDatamapperObject() 149 | { 150 | // directly set private properties if entity extends the datamapper entity class (fast!) 151 | if (is_subclass_of($this->class, '\ProAI\Datamapper\Support\Entity')) { 152 | $class = $this->class; 153 | 154 | return $class::newFromEloquentObject($this); 155 | } 156 | 157 | // set private properties via reflection (slow!) 158 | $reflectionClass = new ReflectionClass($this->class); 159 | 160 | $entity = $reflectionClass->newInstanceWithoutConstructor(); 161 | 162 | // attributes 163 | foreach ($this->mapping['attributes'] as $attribute => $column) { 164 | $this->setProperty( 165 | $reflectionClass, 166 | $entity, 167 | $attribute, 168 | $this->attributes[$column] 169 | ); 170 | } 171 | 172 | // embeddeds 173 | foreach ($this->mapping['embeddeds'] as $name => $embedded) { 174 | $embeddedReflectionClass = new ReflectionClass($embedded['class']); 175 | 176 | $embeddedObject = $embeddedReflectionClass->newInstanceWithoutConstructor(); 177 | foreach ($embedded['attributes'] as $attribute => $column) { 178 | // set property 179 | $this->setProperty( 180 | $embeddedReflectionClass, 181 | $embeddedObject, 182 | $attribute, 183 | $this->attributes[$column] 184 | ); 185 | } 186 | 187 | $this->setProperty( 188 | $reflectionClass, 189 | $entity, 190 | $name, 191 | $embeddedObject 192 | ); 193 | } 194 | 195 | // relations 196 | foreach ($this->mapping['relations'] as $name => $relation) { 197 | // set relation object 198 | if (! empty($this->relations[$name])) { 199 | $relationObject = $this->relations[$name]->toDatamapperObject(); 200 | } elseif (in_array($relation['type'], $this->manyRelations)) { 201 | $relationObject = new ProxyCollection; 202 | } else { 203 | $relationObject = new Proxy; 204 | } 205 | 206 | // set property 207 | $this->setProperty( 208 | $reflectionClass, 209 | $entity, 210 | $name, 211 | $relationObject 212 | ); 213 | } 214 | 215 | return $entity; 216 | } 217 | 218 | /** 219 | * Set a private property of an entity. 220 | * 221 | * @param \ReflectionClass $reflectionClass 222 | * @param object $entity 223 | * @param string $name 224 | * @param mixed $value 225 | * @return void 226 | */ 227 | protected function setProperty(&$reflectionClass, $entity, $name, $value) 228 | { 229 | $property = $reflectionClass->getProperty($name); 230 | $property->setAccessible(true); 231 | $property->setValue($entity, $value); 232 | } 233 | 234 | /** 235 | * Convert model to data transfer object. 236 | * 237 | * @param array $root 238 | * @param array $schema 239 | * @param array $transformations 240 | * @param string $path 241 | * @return object 242 | */ 243 | public function toDataTransferObject($root, $schema, $transformations, $path='') 244 | { 245 | $dto = new DataTransferObject(); 246 | 247 | // get morphed schema 248 | if($this->morphClass) { 249 | $morphKey = '...'.Str::studly($this->morphClass); 250 | if (isset($schema[$morphKey])) { 251 | $schema = $schema[$morphKey]; 252 | } 253 | } 254 | 255 | foreach ($schema as $key => $value) { 256 | // entry is attribute 257 | if (is_numeric($key)) { 258 | // transformation key 259 | $transformationKey = ($path) 260 | ? $root.'.'.$path.'.'.$value 261 | : $root.'.'.$value; 262 | 263 | // set value 264 | if ($value == '__type') { 265 | $dto->{$value} = class_basename($this->class); 266 | } elseif (isset($transformations[$transformationKey])) { 267 | $node = new GraphNode; 268 | $transformations[$transformationKey]($node, $this->attributes); 269 | $dto->{$value} = $node->getValue(); 270 | } elseif (isset($transformations['*.'.$value])) { 271 | $node = new GraphNode; 272 | $transformations['*.'.$value]($node, $this->attributes); 273 | $dto->{$value} = $node->getValue(); 274 | } else { 275 | $columnName = $this->getColumnName($value); 276 | if (isset($this->attributes[$columnName])) { 277 | $dto->{$value} = $this->attributes[$columnName]; 278 | } 279 | } 280 | } 281 | 282 | // entry is relation 283 | if (! is_numeric($key) && isset($this->relations[$key])) { 284 | // set value and transform childs to dtos 285 | $newPath = ($path) ? $path.'.'.$key : $key; 286 | $dto->{$key} = $this->relations[$key]->toDataTransferObject( 287 | $root, 288 | $value, 289 | $transformations, 290 | $newPath 291 | ); 292 | } 293 | } 294 | 295 | return $dto; 296 | } 297 | 298 | /** 299 | * Convert model to plain old php object. 300 | * 301 | * @param \ProAI\Datamapper\Contracts\Entity $entity 302 | * @param string $lastObjectId 303 | * @param \ProAI\Datamapper\Eloquent\Model $lastEloquentModel 304 | * @return \ProAI\Datamapper\Eloquent\Model 305 | */ 306 | public static function newFromDatamapperObject(EntityContract $entity, $lastObjectId = null, $lastEloquentModel = null) 307 | { 308 | // directly get private properties if entity extends the datamapper entity class (fast!) 309 | if ($entity instanceof \ProAI\Datamapper\Support\Entity) { 310 | return $entity->toEloquentObject($lastObjectId, $lastEloquentModel); 311 | } 312 | 313 | // get private properties via reflection (slow!) 314 | $class = get_mapped_model(get_class($entity)); 315 | 316 | $eloquentModel = new $class; 317 | 318 | $reflectionObject = new ReflectionObject($entity); 319 | 320 | $mapping = $eloquentModel->getMapping(); 321 | 322 | // attributes 323 | foreach ($mapping['attributes'] as $attribute => $column) { 324 | if (! $eloquentModel->isAutomaticallyUpdatedDate($column)) { 325 | // get property 326 | $property = $eloquentModel->getProperty( 327 | $reflectionObject, 328 | $entity, 329 | $attribute 330 | ); 331 | 332 | // set attribute 333 | $eloquentModel->setAttribute($column, $property); 334 | } 335 | } 336 | 337 | // embeddeds 338 | foreach ($mapping['embeddeds'] as $name => $embedded) { 339 | $embeddedObject = $eloquentModel->getProperty($reflectionObject, $entity, $name); 340 | 341 | 342 | if (! empty($embeddedObject)) { 343 | $embeddedReflectionObject = new ReflectionObject($embeddedObject); 344 | 345 | foreach ($embedded['attributes'] as $attribute => $column) { 346 | // get property 347 | $property = $eloquentModel->getProperty( 348 | $embeddedReflectionObject, 349 | $embeddedObject, 350 | $attribute 351 | ); 352 | 353 | // set attribute 354 | $eloquentModel->setAttribute($column, $property); 355 | } 356 | } 357 | } 358 | 359 | // relations 360 | foreach ($mapping['relations'] as $name => $relation) { 361 | $relationObject = $eloquentModel->getProperty( 362 | $reflectionObject, 363 | $entity, 364 | $name 365 | ); 366 | 367 | if (! empty($relationObject) && ! $relationObject instanceof \ProAI\Datamapper\Contracts\Proxy) { 368 | // set relation 369 | if ($relationObject instanceof \ProAI\Datamapper\Support\Collection) { 370 | $value = EloquentCollection::newFromDatamapperObject($relationObject, $this, $eloquentModel); 371 | } elseif (spl_object_hash($relationObject) == $lastObjectId) { 372 | $value = $lastEloquentModel; 373 | } else { 374 | $value = EloquentModel::newFromDatamapperObject($relationObject, spl_object_hash($this), $eloquentModel); 375 | } 376 | 377 | $eloquentModel->setRelation($name, $value); 378 | } 379 | } 380 | 381 | return $eloquentModel; 382 | } 383 | 384 | /** 385 | * Check if attribute is auto generated and updated date. 386 | * 387 | * @param string $attribute 388 | * @return boolean 389 | */ 390 | public function isAutomaticallyUpdatedDate($attribute) 391 | { 392 | // soft deletes 393 | if (in_array('Illuminate\Database\Eloquent\SoftDeletes', class_uses(static::class)) && $attribute == $this->getDeletedAtColumn()) { 394 | return true; 395 | } 396 | 397 | // timestamps 398 | if ($this->timestamps && ($attribute == $this->getCreatedAtColumn() || $attribute == $this->getUpdatedAtColumn())) { 399 | return true; 400 | } 401 | 402 | return false; 403 | } 404 | 405 | /** 406 | * Get a private property of an entity. 407 | * 408 | * @param \ReflectionObject $reflectionObject 409 | * @param object $entity 410 | * @param string $name 411 | * @param mixed $value 412 | * @return mixed 413 | */ 414 | protected function getProperty($reflectionObject, $entity, $name) 415 | { 416 | $property = $reflectionObject->getProperty($name); 417 | $property->setAccessible(true); 418 | return $property->getValue($entity); 419 | } 420 | 421 | /** 422 | * Update auto inserted/updated fields. 423 | * 424 | * @param \ProAI\Datamapper\Contracts\Entity $entity 425 | * @param string $action 426 | * @return void 427 | */ 428 | public function afterSaving($entity, $action) 429 | { 430 | // set private properties via reflection (slow!) 431 | $reflectionClass = new ReflectionClass($this->class); 432 | 433 | if ($updateFields = $this->getAutomaticallyUpdatedFields($entity, $action)) { 434 | 435 | // attributes 436 | foreach ($this->mapping['attributes'] as $attribute => $column) { 437 | if (in_array($column, $updateFields)) { 438 | $this->setProperty( 439 | $reflectionClass, 440 | $entity, 441 | $attribute, 442 | $this->attributes[$column] 443 | ); 444 | } 445 | } 446 | 447 | // embeddeds 448 | foreach ($this->mapping['embeddeds'] as $name => $embedded) { 449 | $embeddedReflectionClass = new ReflectionClass($embedded['class']); 450 | 451 | $embeddedObject = $this->getProperty( 452 | $reflectionClass, 453 | $entity, 454 | $name 455 | ); 456 | 457 | if (empty($embeddedObject)) { 458 | $embeddedObject = $embeddedReflectionClass->newInstanceWithoutConstructor(); 459 | } 460 | 461 | foreach ($embedded['attributes'] as $attribute => $column) { 462 | if (in_array($column, $updateFields)) { 463 | $this->setProperty( 464 | $embeddedReflectionClass, 465 | $embeddedObject, 466 | $attribute, 467 | $this->attributes[$column] 468 | ); 469 | } 470 | } 471 | 472 | $this->setProperty( 473 | $reflectionClass, 474 | $entity, 475 | $name, 476 | $embeddedObject 477 | ); 478 | } 479 | 480 | } 481 | } 482 | 483 | /** 484 | * Get auto inserted/updated fields. 485 | * 486 | * @param object $entity 487 | * @param string $action 488 | * @return void 489 | */ 490 | protected function getAutomaticallyUpdatedFields($entity, $action) 491 | { 492 | $updateFields = []; 493 | 494 | // auto increment 495 | if ($action == 'insert' && $this->incrementing) { 496 | $updateFields[] = $this->getKeyName(); 497 | } 498 | 499 | // auto uuid 500 | if ($action == 'insert' && method_exists($this, 'bootAutoUuid')) { 501 | $updateFields = array_merge($this->autoUuids, $updateFields); 502 | } 503 | 504 | // timestamps 505 | if ($this->timestamps) { 506 | if ($action == 'insert') { 507 | $updateFields[] = $this->getCreatedAtColumn(); 508 | } 509 | $updateFields[] = $this->getUpdatedAtColumn(); 510 | } 511 | 512 | // soft deletes 513 | if ($action == 'update' && method_exists($this, 'bootSoftDeletes')) { 514 | $updateFields[] = $this->getDeletedAtColumn(); 515 | } 516 | 517 | return $updateFields; 518 | } 519 | 520 | /** 521 | * Get the mapping data. 522 | * 523 | * @return array 524 | */ 525 | public function getMapping() 526 | { 527 | return $this->mapping; 528 | } 529 | 530 | /** 531 | * Get column name of a schema name. 532 | * 533 | * @param string $name 534 | * @return string 535 | */ 536 | protected function getColumnName($name) 537 | { 538 | // check attributes for given name 539 | if (isset($this->mapping['attributes'][$name])) { 540 | return $this->mapping['attributes'][$name]; 541 | } 542 | 543 | // check embeddeds for given name 544 | foreach($this->mapping['embeddeds'] as $embedded) { 545 | // check for embedded attributes 546 | if (isset($embedded['attributes'][$name])) { 547 | return $embedded['attributes'][$name]; 548 | } 549 | 550 | // check for embedded attributes using embedded column prefix 551 | if ($embedded['columnPrefix'] && strpos($name, $embedded['columnPrefix']) === 0) { 552 | $embeddedName = substr($name, strlen($embedded['columnPrefix'])); 553 | 554 | if (isset($embedded['attributes'][$embeddedName])) { 555 | return $embedded['attributes'][$embeddedName]; 556 | } 557 | } 558 | } 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /src/EntityManager.php: -------------------------------------------------------------------------------- 1 | newQuery(Builder::RETURN_TYPE_DATAMAPPER); 34 | } 35 | 36 | /** 37 | * Get a new schema query instance. 38 | * 39 | * @param string $class 40 | * @return \ProAI\Datamapper\Eloquent\GraphBuilder 41 | */ 42 | public function newGraphQuery($class) 43 | { 44 | $class = get_real_entity($class); 45 | 46 | $eloquentModel = get_mapped_model($class); 47 | 48 | return (new $eloquentModel)->newGraphQuery(); 49 | } 50 | 51 | /** 52 | * Create an entity object. 53 | * 54 | * @param object $entity 55 | * @return void 56 | */ 57 | public function insert($entity) 58 | { 59 | $eloquentModel = $this->getEloquentModel($entity); 60 | 61 | $this->updateRelations($eloquentModel, 'insert'); 62 | 63 | $eloquentModel->save(); 64 | 65 | $eloquentModel->afterSaving($entity, 'insert'); 66 | } 67 | 68 | /** 69 | * Update an entity object. 70 | * 71 | * @param object $entity 72 | * @return void 73 | */ 74 | public function update($entity) 75 | { 76 | $eloquentModel = $this->getEloquentModel($entity, true); 77 | 78 | $this->updateRelations($eloquentModel, 'update'); 79 | 80 | $eloquentModel->save(); 81 | 82 | $eloquentModel->afterSaving($entity, 'update'); 83 | } 84 | 85 | /** 86 | * Delete an entity object. 87 | * 88 | * @param object $entity 89 | * @return void 90 | */ 91 | public function delete($entity) 92 | { 93 | $eloquentModel = $this->getEloquentModel($entity, true); 94 | 95 | $this->updateRelations($eloquentModel, 'delete'); 96 | 97 | $eloquentModel->delete(); 98 | } 99 | 100 | /** 101 | * Update relations. 102 | * 103 | * @param \ProAI\Datamapper\Eloquent\Model $eloquentModel 104 | * @param string $action 105 | * @return void 106 | */ 107 | protected function updateRelations($eloquentModel, $action) 108 | { 109 | $mapping = $eloquentModel->getMapping(); 110 | $eloquentRelations = $eloquentModel->getRelations(); 111 | 112 | foreach($mapping['relations'] as $name => $relationMapping) { 113 | if (isset($eloquentRelations[$name])) { 114 | $this->updateRelation($eloquentModel, $name, $relationMapping, $action); 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Update a relation. 121 | * 122 | * @param \ProAI\Datamapper\Eloquent\Model $eloquentModel 123 | * @param string $name 124 | * @param array $relationMapping 125 | * @param string $action 126 | * @return void 127 | */ 128 | protected function updateRelation($eloquentModel, $name, $relationMapping, $action) 129 | { 130 | // set foreign key for belongsTo/morphTo relation 131 | if ($relationMapping['type'] == 'belongsTo' || $relationMapping['type'] == 'morphTo') { 132 | $this->updateBelongsToRelation($eloquentModel, $name, $action); 133 | } 134 | 135 | // set foreign keys for belongsToMany/morphToMany relation 136 | if (($relationMapping['type'] == 'belongsToMany' || $relationMapping['type'] == 'morphToMany') && ! $relationMapping['inverse']) { 137 | $this->updateBelongsToManyRelation($eloquentModel, $name, $action); 138 | } 139 | } 140 | 141 | /** 142 | * Update a belongsTo or morphTo relation. 143 | * 144 | * @param \ProAI\Datamapper\Eloquent\Model $eloquentModel 145 | * @param string $name 146 | * @param string $action 147 | * @return void 148 | */ 149 | protected function updateBelongsToRelation($eloquentModel, $name, $action) 150 | { 151 | if ($action == 'insert' || $action == 'update') { 152 | $eloquentModel->{$name}()->associate($eloquentModel->getRelation($name)); 153 | } 154 | } 155 | 156 | /** 157 | * Update a belongsToMany or morphToMany relation. 158 | * 159 | * @param \ProAI\Datamapper\Eloquent\Model $eloquentModel 160 | * @param string $name 161 | * @param string $action 162 | * @return void 163 | */ 164 | protected function updateBelongsToManyRelation($eloquentModel, $name, $action) 165 | { 166 | $eloquentCollection = $eloquentModel->getRelation($name); 167 | 168 | if (! $eloquentCollection instanceof \Illuminate\Database\Eloquent\Collection) { 169 | throw new Exception("Many-to-many relation '".$name."' is not a valid collection"); 170 | } 171 | 172 | // get related keys 173 | $keys = []; 174 | 175 | foreach($eloquentCollection as $item) { 176 | $keys[] = $item->getKey(); 177 | } 178 | 179 | // attach/sync/detach keys 180 | if ($action == 'insert') { 181 | $eloquentModel->{$name}()->attach($keys); 182 | } 183 | if ($action == 'update') { 184 | $eloquentModel->{$name}()->sync($keys); 185 | } 186 | if ($action == 'delete') { 187 | $eloquentModel->{$name}()->detach($keys); 188 | } 189 | } 190 | 191 | /** 192 | * Delete an entity object. 193 | * 194 | * @param object $entity 195 | * @return \ProAI\Datamapper\Eloquent\Model 196 | */ 197 | protected function getEloquentModel($entity, $exists=false) 198 | { 199 | if (empty($entity)) { 200 | throw new Exception('Object transfered to EntityManager is empty'); 201 | } 202 | 203 | if (! is_object($entity)) { 204 | throw new Exception('Object transfered to EntityManager is not an object'); 205 | } 206 | 207 | $eloquentModel = Model::newFromDatamapperObject($entity); 208 | 209 | $eloquentModel->exists = $exists; 210 | 211 | return $eloquentModel; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/Metadata/AnnotationLoader.php: -------------------------------------------------------------------------------- 1 | files = $files; 27 | $this->path = $path; 28 | } 29 | 30 | 31 | /** 32 | * Register all annotations. 33 | * 34 | * @return void 35 | */ 36 | public function registerAll() 37 | { 38 | foreach ($this->files->allFiles($this->path) as $file) { 39 | AnnotationRegistry::registerFile($file->getRealPath()); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Metadata/ClassFinder.php: -------------------------------------------------------------------------------- 1 | finder = $finder; 33 | } 34 | 35 | /** 36 | * Get all classes for a given namespace. 37 | * 38 | * @param string $namespace 39 | * @return array 40 | */ 41 | public function getClassesFromNamespace($namespace = null) 42 | { 43 | $namespace = $namespace ?: $this->getAppNamespace(); 44 | 45 | $path = $this->convertNamespaceToPath($namespace); 46 | 47 | return $this->finder->findClasses($path); 48 | } 49 | 50 | /** 51 | * Convert given namespace to file path. 52 | * 53 | * @param string $namespace 54 | * @return string|null 55 | */ 56 | protected function convertNamespaceToPath($namespace) 57 | { 58 | // strip app namespace 59 | $appNamespace = $this->getAppNamespace(); 60 | 61 | if (substr($namespace, 0, strlen($appNamespace)) != $appNamespace) { 62 | return null; 63 | } 64 | 65 | $subNamespace = substr($namespace, strlen($appNamespace)); 66 | 67 | // replace \ with / to get the correct file path 68 | $subPath = str_replace('\\', '/', $subNamespace); 69 | 70 | // create path 71 | return app('path') . '/' . $subPath; 72 | } 73 | 74 | /** 75 | * Get the application namespace. 76 | * 77 | * @return string 78 | * 79 | * @throws \RuntimeException 80 | */ 81 | public function getAppNamespace() 82 | { 83 | if (! is_null($this->namespace)) { 84 | return $this->namespace; 85 | } 86 | 87 | $composer = json_decode(file_get_contents(base_path().'/composer.json'), true); 88 | 89 | foreach ((array) data_get($composer, 'autoload.psr-4') as $namespace => $path) { 90 | foreach ((array) $path as $pathChoice) { 91 | if (realpath(app('path')) == realpath(base_path().'/'.$pathChoice)) { 92 | return $this->namespace = $namespace; 93 | } 94 | } 95 | } 96 | 97 | throw new RuntimeException('Unable to detect application namespace.'); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Metadata/Definitions/Attribute.php: -------------------------------------------------------------------------------- 1 | null, 14 | 'columnName' => null, 15 | 'versioned' => false, 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Metadata/Definitions/Column.php: -------------------------------------------------------------------------------- 1 | null, 14 | 'type' => null, 15 | 'primary' => false, 16 | 'index' => false, 17 | 'unique' => false, 18 | 'nullable' => false, 19 | 'default' => null, 20 | 'options' => [], 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /src/Metadata/Definitions/Definition.php: -------------------------------------------------------------------------------- 1 | keys), array_keys($array))) { 21 | throw new UnexpectedValueException('Missing value(s) '.implode(", ", $diff).' in metadata definition '.get_class($this).'.'); 22 | } 23 | 24 | foreach ($array as $name => $value) { 25 | $this[$name] = $value; 26 | } 27 | } 28 | 29 | /** 30 | * Do not allow to set more definitions. 31 | * 32 | * @param mixed $key 33 | * @param mixed $newval 34 | * @return void 35 | */ 36 | public function offsetSet($key, $newval) 37 | { 38 | if ($def = in_array($key, array_keys($this->keys))) { 39 | parent::offsetSet($key, $newval); 40 | } else { 41 | throw new UnexpectedValueException($key.' is not defined in metadata definition '.get_class($this).'.'); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Metadata/Definitions/EmbeddedClass.php: -------------------------------------------------------------------------------- 1 | null, 14 | 'class' => null, 15 | 'columnPrefix' => null, 16 | 'attributes' => [], 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /src/Metadata/Definitions/Entity.php: -------------------------------------------------------------------------------- 1 | null, 14 | 'morphClass' => null, 15 | 'table' => null, 16 | 'versionTable' => null, 17 | 18 | 'softDeletes' => false, 19 | 'timestamps' => false, 20 | 21 | 'touches' => [], 22 | 'with' => [], 23 | 24 | 'columns' => [], 25 | 'attributes' => [], 26 | 'embeddeds' => [], 27 | 'relations' => [], 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /src/Metadata/Definitions/Relation.php: -------------------------------------------------------------------------------- 1 | null, 14 | 'type' => null, 15 | 'relatedEntity' => null, 16 | 'pivotTable' => null, 17 | 'options' => [], 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /src/Metadata/Definitions/Table.php: -------------------------------------------------------------------------------- 1 | null, 14 | 'columns' => [], 15 | ]; 16 | } 17 | -------------------------------------------------------------------------------- /src/Metadata/EntityScanner.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 57 | 58 | $this->validator = $validator; 59 | } 60 | 61 | /** 62 | * Build metadata from all entity classes. 63 | * 64 | * @param array $classes 65 | * @param boolean $namespaceTablenames 66 | * @param boolean $morphClassAbbreviations 67 | * @return array 68 | */ 69 | public function scan($classes, $namespaceTablenames=true, $morphClassAbbreviations=true) 70 | { 71 | $this->namespaceTablenames = $namespaceTablenames; 72 | $this->morphClassAbbreviations = $morphClassAbbreviations; 73 | 74 | $metadata = []; 75 | 76 | foreach ($classes as $class) { 77 | $entityMetadata = $this->parseClass($class); 78 | 79 | if ($entityMetadata) { 80 | $metadata[$class] = $entityMetadata; 81 | } 82 | } 83 | 84 | // validate pivot tables 85 | $this->validator->validatePivotTables($metadata); 86 | 87 | // generate morphable classes 88 | $this->generateMorphableClasses($metadata); 89 | 90 | return $metadata; 91 | } 92 | 93 | /** 94 | * Parse a class. 95 | * 96 | * @param string $class 97 | * @return array|null 98 | */ 99 | public function parseClass($class) 100 | { 101 | $reflectionClass = new ReflectionClass($class); 102 | 103 | // check if class is entity 104 | if ($this->reader->getClassAnnotation($reflectionClass, '\ProAI\Datamapper\Annotations\Entity')) { 105 | return $this->parseEntity($class); 106 | } else { 107 | return null; 108 | } 109 | } 110 | 111 | /** 112 | * Parse an entity class. 113 | * 114 | * @param string $class 115 | * @return array 116 | */ 117 | public function parseEntity($class) 118 | { 119 | $reflectionClass = new ReflectionClass($class); 120 | 121 | // scan class annotations 122 | $classAnnotations = $this->reader->getClassAnnotations($reflectionClass); 123 | 124 | // init class metadata 125 | $entityMetadata = new EntityDefinition([ 126 | 'class' => $class, 127 | 'morphClass' => $this->generateMorphClass($class), 128 | 'table' => new TableDefinition([ 129 | 'name' => $this->generateTableName($class), 130 | 'columns' => [], 131 | ]), 132 | 'versionTable' => false, 133 | 134 | 'softDeletes' => false, 135 | 'timestamps' => false, 136 | 137 | 'touches' => [], 138 | 'with' => [], 139 | 140 | 'columns' => [], 141 | 'attributes' => [], 142 | 'embeddeds' => [], 143 | 'relations' => [], 144 | ]); 145 | 146 | // find entity parameters and plugins 147 | foreach ($classAnnotations as $annotation) { 148 | // entity parameters 149 | if ($annotation instanceof \ProAI\Datamapper\Annotations\Entity) { 150 | if (! empty($annotation->morphClass)) { 151 | $entityMetadata['morphClass'] = $annotation->morphClass; 152 | } 153 | if (! empty($annotation->touches)) { 154 | $entityMetadata['touches'] = $annotation->touches; 155 | } 156 | if (! empty($annotation->with)) { 157 | $entityMetadata['with'] = $annotation->with; 158 | } 159 | } 160 | 161 | // softdeletes 162 | if ($annotation instanceof \ProAI\Datamapper\Annotations\SoftDeletes) { 163 | $entityMetadata['softDeletes'] = true; 164 | } 165 | 166 | // table name 167 | if ($annotation instanceof \ProAI\Datamapper\Annotations\Table) { 168 | $entityMetadata['table']['name'] = $annotation->name; 169 | } 170 | 171 | // timestamps 172 | if ($annotation instanceof \ProAI\Datamapper\Annotations\Timestamps) { 173 | $entityMetadata['timestamps'] = true; 174 | } 175 | } 176 | 177 | // find versionable annotation (2nd loop, because table name is required) 178 | foreach ($classAnnotations as $annotation) { 179 | if ($annotation instanceof \ProAI\Datamapper\Annotations\Versionable) { 180 | $entityMetadata['versionTable'] = new TableDefinition([ 181 | 'name' => $entityMetadata['table']['name'] . '_version', 182 | 'columns' => [], 183 | ]); 184 | } 185 | } 186 | 187 | // find columns and embedded classes 188 | foreach ($reflectionClass->getProperties() as $reflectionProperty) { 189 | $name = $this->getSanitizedName($reflectionProperty->getName(), $entityMetadata['class']); 190 | $propertyAnnotations = $this->reader->getPropertyAnnotations($reflectionProperty); 191 | 192 | foreach ($propertyAnnotations as $annotation) { 193 | // column 194 | if ($annotation instanceof \ProAI\Datamapper\Annotations\Column) { 195 | $this->setAdditionalColumnProperties($name, $annotation, $propertyAnnotations); 196 | 197 | $this->parseColumn($name, $annotation, $entityMetadata, false, true); 198 | } 199 | 200 | // embedded class 201 | if ($annotation instanceof \ProAI\Datamapper\Annotations\Embedded) { 202 | $this->parseEmbeddedClass($name, $annotation, $entityMetadata, true); 203 | } 204 | } 205 | } 206 | 207 | // check primary key 208 | $this->validator->validatePrimaryKey($entityMetadata); 209 | 210 | // find relationships (2nd loop, because primary key column is required for foreign keys) 211 | foreach ($reflectionClass->getProperties() as $reflectionProperty) { 212 | $name = $this->getSanitizedName($reflectionProperty->getName(), $entityMetadata['class']); 213 | $propertyAnnotations = $this->reader->getPropertyAnnotations($reflectionProperty); 214 | 215 | foreach ($propertyAnnotations as $annotation) { 216 | // column 217 | if ($annotation instanceof \ProAI\Datamapper\Annotations\Column) { 218 | $this->setAdditionalColumnProperties($name, $annotation, $propertyAnnotations); 219 | 220 | $entityMetadata['attributes'][] = $this->parseColumn($name, $annotation, $entityMetadata); 221 | } 222 | 223 | // embedded class 224 | if ($annotation instanceof \ProAI\Datamapper\Annotations\Embedded) { 225 | $entityMetadata['embeddeds'][] = $this->parseEmbeddedClass($name, $annotation, $entityMetadata); 226 | } 227 | 228 | // relation 229 | if ($annotation instanceof \ProAI\Datamapper\Annotations\Relation) { 230 | $entityMetadata['relations'][] = $this->parseRelation($name, $annotation, $entityMetadata); 231 | } 232 | } 233 | } 234 | 235 | // check timestamps extension 236 | if (! empty($entityMetadata['timestamps'])) { 237 | $this->validator->validateTimestamps($entityMetadata); 238 | } 239 | 240 | // check softDeletes extension 241 | if (! empty($entityMetadata['softDeletes'])) { 242 | $this->validator->validateSoftDeletes($entityMetadata); 243 | } 244 | 245 | // check version extension 246 | if (! empty($entityMetadata['versionTable'])) { 247 | $this->validator->validateVersionTable($entityMetadata); 248 | } 249 | 250 | return $entityMetadata; 251 | } 252 | 253 | /** 254 | * Parse an embedded class. 255 | * 256 | * @param string $name 257 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 258 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 259 | * @return \ProAI\Datamapper\Metadata\Definitions\EmbeddedClass 260 | */ 261 | protected function parseEmbeddedClass($name, Annotation $annotation, EntityDefinition &$entityMetadata, $primaryKeyOnly = false) 262 | { 263 | // check if related class is valid 264 | $annotation->class = $this->getRealEntity($annotation->class, $entityMetadata['class']); 265 | 266 | $reflectionClass = new ReflectionClass($annotation->class); 267 | 268 | $classAnnotations = $this->reader->getClassAnnotations($reflectionClass); 269 | 270 | // check if class is embedded class 271 | $this->validator->validateEmbeddedClass($annotation->class, $classAnnotations); 272 | 273 | $embeddedColumnPrefix = ($annotation->columnPrefix || $annotation->columnPrefix === false) 274 | ? $annotation->columnPrefix 275 | : $name; 276 | 277 | $embeddedClassMetadata = new EmbeddedClassDefinition([ 278 | 'name' => $name, 279 | 'class' => $annotation->class, 280 | 'columnPrefix' => $embeddedColumnPrefix, 281 | 'attributes' => [], 282 | ]); 283 | 284 | // scan property annotations 285 | foreach ($reflectionClass->getProperties() as $reflectionProperty) { 286 | $name = $this->getSanitizedName($reflectionProperty->getName(), $entityMetadata['class']); 287 | 288 | $propertyAnnotations = $this->reader->getPropertyAnnotations($reflectionProperty); 289 | 290 | foreach ($propertyAnnotations as $annotation) { 291 | // property is column 292 | if ($annotation instanceof \ProAI\Datamapper\Annotations\Column) { 293 | $this->setAdditionalColumnProperties($name, $annotation, $propertyAnnotations, true, $embeddedColumnPrefix); 294 | 295 | $embeddedClassMetadata['attributes'][] = $this->parseColumn($name, $annotation, $entityMetadata, true, $primaryKeyOnly); 296 | } 297 | } 298 | } 299 | 300 | return $embeddedClassMetadata; 301 | } 302 | 303 | /** 304 | * Set properties of column annotation related annotations. 305 | * 306 | * @param string $name 307 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 308 | * @param array $propertyAnnotations 309 | * @param boolean $embedded 310 | * @param mixed $columnPrefix 311 | * @return void 312 | */ 313 | protected function setAdditionalColumnProperties($name, Annotation &$annotation, array $propertyAnnotations, $embedded=false, $columnPrefix=false) 314 | { 315 | // scan for primary and versioned property 316 | foreach ($propertyAnnotations as $subAnnotation) { 317 | // set primary key 318 | if ($subAnnotation instanceof \ProAI\Datamapper\Annotations\Id) { 319 | $annotation->primary = true; 320 | } 321 | // set auto increment 322 | if ($subAnnotation instanceof \ProAI\Datamapper\Annotations\AutoIncrement) { 323 | $annotation->autoIncrement = true; 324 | } 325 | // set auto increment 326 | if ($subAnnotation instanceof \ProAI\Datamapper\Annotations\AutoUuid) { 327 | $annotation->autoUuid = true; 328 | } 329 | // set versioned 330 | if ($subAnnotation instanceof \ProAI\Datamapper\Annotations\Versioned) { 331 | $annotation->versioned = true; 332 | } 333 | } 334 | 335 | // set column name 336 | $annotation->name = $this->getColumnName($annotation->name ?: $name, $columnPrefix); 337 | } 338 | 339 | /** 340 | * Parse a column. 341 | * 342 | * @param string $name 343 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 344 | * @param \ProAI\Datamapper\Metadata\Definitions\Class $entityMetadata 345 | * @param boolean $embedded 346 | * @return \ProAI\Datamapper\Metadata\Definitions\Attribute 347 | */ 348 | protected function parseColumn($name, Annotation $annotation, EntityDefinition &$entityMetadata, $embedded=false, $primaryKeyOnly = false) 349 | { 350 | if ($annotation->primary == $primaryKeyOnly) { 351 | // set column data 352 | if (! empty($entityMetadata['versionTable']) && $annotation->versioned) { 353 | $entityMetadata['versionTable']['columns'][] = $this->generateColumn($name, $annotation); 354 | } else { 355 | $entityMetadata['table']['columns'][] = $this->generateColumn($name, $annotation); 356 | } 357 | 358 | // set up version feature 359 | if (! empty($entityMetadata['versionTable']) && ! $annotation->versioned && $annotation->primary) { 360 | $this->generateVersionTable($name, $annotation, $entityMetadata); 361 | } 362 | } 363 | 364 | return $this->generateAttribute($name, $annotation); 365 | } 366 | 367 | /** 368 | * Generate a column. 369 | * 370 | * @param string $name 371 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 372 | * @return \ProAI\Datamapper\Metadata\Definitions\Column 373 | */ 374 | protected function generateColumn($name, $annotation) 375 | { 376 | // check if column type is valid 377 | $this->validator->validateColumnType($annotation->type); 378 | 379 | // add column 380 | return new ColumnDefinition([ 381 | 'name' => $annotation->name, 382 | 'type' => $annotation->type, 383 | 'nullable' => $annotation->nullable, 384 | 'default' => $annotation->default, 385 | 'primary' => $annotation->primary, 386 | 'unique' => $annotation->unique, 387 | 'index' => $annotation->index, 388 | 'options' => $this->generateAttributeOptionsArray($annotation) 389 | ]); 390 | } 391 | 392 | /** 393 | * Generate versioning table. 394 | * 395 | * @param string $name 396 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 397 | * @param \ProAI\Datamapper\Metadata\Definitions\Class $entityMetadata 398 | * @return void 399 | */ 400 | protected function generateVersionTable($name, Annotation $annotation, EntityDefinition &$entityMetadata) 401 | { 402 | $annotation = clone $annotation; 403 | $annotation->name ='ref_' . $annotation->name; 404 | $annotation->autoIncrement = false; 405 | 406 | // copy primary key to version table 407 | $entityMetadata['versionTable']['columns'][] = $this->generateColumn($name, $annotation); 408 | } 409 | 410 | /** 411 | * Generate an attribute. 412 | * 413 | * @param string $name 414 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 415 | * @return \ProAI\Datamapper\Metadata\Definitions\Attribute 416 | */ 417 | protected function generateAttribute($name, $annotation) 418 | { 419 | // add attribute 420 | return new AttributeDefinition([ 421 | 'name' => $name, 422 | 'columnName' => $annotation->name, 423 | 'versioned' => $annotation->versioned 424 | ]); 425 | } 426 | 427 | /** 428 | * Generate an options array for an attribute. 429 | * 430 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 431 | * @return array 432 | */ 433 | protected function generateAttributeOptionsArray(Annotation $annotation) 434 | { 435 | $options = []; 436 | 437 | // length option 438 | if ($annotation->type == 'string' || $annotation->type == 'char' || $annotation->type == 'binary') { 439 | $options['length'] = $annotation->length; 440 | } 441 | 442 | // fixed option 443 | if ($annotation->type == 'binary' && $annotation->length == 16) { 444 | $options['fixed'] = $annotation->fixed; 445 | $options['autoUuid'] = $annotation->autoUuid; 446 | } 447 | 448 | // unsigned and autoIncrement option 449 | if ($annotation->type == 'smallInteger' || $annotation->type == 'integer' || $annotation->type == 'bigInteger') { 450 | $options['unsigned'] = $annotation->unsigned; 451 | $options['autoIncrement'] = $annotation->autoIncrement; 452 | } 453 | 454 | // scale and precision option 455 | if ($annotation->type == 'decimal') { 456 | $options['scale'] = $annotation->scale; 457 | $options['precision'] = $annotation->precision; 458 | } 459 | 460 | return $options; 461 | } 462 | 463 | /** 464 | * Parse a relationship. 465 | * 466 | * @param string $name 467 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 468 | * @param \ProAI\Datamapper\Metadata\Definitions\Class $entityMetadata 469 | * @return \ProAI\Datamapper\Metadata\Definitions\Relation 470 | */ 471 | protected function parseRelation($name, Annotation $annotation, EntityDefinition &$entityMetadata) 472 | { 473 | // check if relation type is valid 474 | $this->validator->validateRelationType($annotation->type); 475 | 476 | // check if we need to add base namespace from configuration 477 | $annotation->relatedEntity = $annotation->relatedEntity 478 | ? $this->getRealEntity($annotation->relatedEntity, $entityMetadata['class']) 479 | : null; 480 | $annotation->throughEntity = $annotation->throughEntity 481 | ? $this->getRealEntity($annotation->throughEntity, $entityMetadata['class']) 482 | : null; 483 | 484 | // change morphedByMany to inverse morphToMany 485 | if ($annotation->type == 'morphedByMany') { 486 | $annotation->type = 'morphToMany'; 487 | $annotation->inverse = true; 488 | } 489 | 490 | // create extra columns for belongsTo 491 | if ($annotation->type == 'belongsTo') { 492 | $this->generateBelongsToColumns($name, $annotation, $entityMetadata); 493 | } 494 | 495 | // create extra columns for morphTo 496 | if ($annotation->type == 'morphTo') { 497 | $this->generateMorphToColumns($name, $annotation, $entityMetadata); 498 | } 499 | 500 | $pivotTable = null; 501 | 502 | // create pivot table for belongsToMany 503 | if ($annotation->type == 'belongsToMany') { 504 | $pivotTable = $this->generateBelongsToManyPivotTable($name, $annotation, $entityMetadata); 505 | } 506 | 507 | // create pivot table for morphToMany 508 | if ($annotation->type == 'morphToMany') { 509 | $pivotTable = $this->generateMorphToManyPivotTable($name, $annotation, $entityMetadata); 510 | } 511 | 512 | // add relation 513 | return new RelationDefinition([ 514 | 'name' => $name, 515 | 'type' => $annotation->type, 516 | 'relatedEntity' => $annotation->relatedEntity, 517 | 'pivotTable' => $pivotTable, 518 | 'options' => $this->generateRelationOptionsArray($name, $annotation, $entityMetadata) 519 | ]); 520 | } 521 | 522 | /** 523 | * Generate an options array for a relation. 524 | * 525 | * @param string $name 526 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 527 | * @param \ProAI\Datamapper\Metadata\Definitions\Class $entityMetadata 528 | * @return array 529 | */ 530 | protected function generateRelationOptionsArray($name, Annotation $annotation, EntityDefinition &$entityMetadata) 531 | { 532 | $options = []; 533 | 534 | // belongsTo relation 535 | if ($annotation->type == 'belongsTo') { 536 | $options['relatedForeignKey'] = $annotation->relatedForeignKey ?: $this->generateKey($annotation->relatedEntity); 537 | $options['localKey'] = $annotation->localKey ?: 'id'; 538 | $options['relation'] = $annotation->relation; 539 | } 540 | 541 | // belongsToMany relation 542 | if ($annotation->type == 'belongsToMany') { 543 | $options['pivotTable'] = $annotation->pivotTable ?: $this->generatePivotTablename($entityMetadata['class'], $annotation->relatedEntity, $annotation->inverse); 544 | $options['localPivotKey'] = $annotation->localPivotKey ?: $this->generateKey($entityMetadata['class']); 545 | $options['relatedPivotKey'] = $annotation->relatedPivotKey ?: $this->generateKey($annotation->relatedEntity); 546 | $options['relation'] = $annotation->relation; 547 | } 548 | 549 | // hasMany relation 550 | if ($annotation->type == 'hasMany') { 551 | $options['localForeignKey'] = $annotation->localForeignKey ?: $this->generateKey($entityMetadata['class']); 552 | $options['localKey'] = $annotation->localKey ?: 'id'; 553 | } 554 | 555 | // hasManyThrough relation 556 | if ($annotation->type == 'hasManyThrough') { 557 | $options['throughEntity'] = $annotation->throughEntity; 558 | $options['localForeignKey'] = $annotation->localForeignKey ?: $this->generateKey($entityMetadata['class']); 559 | $options['throughForeignKey'] = $annotation->throughForeignKey ?: $this->generateKey($annotation->throughEntity); 560 | } 561 | 562 | // hasOne relation 563 | if ($annotation->type == 'hasOne') { 564 | $options['localForeignKey'] = $annotation->localForeignKey ?: $this->generateKey($entityMetadata['class']); 565 | $options['localKey'] = $annotation->localKey ?: 'id'; 566 | } 567 | 568 | // morphMany relation 569 | if ($annotation->type == 'morphMany') { 570 | $options['morphName'] = $annotation->morphName ?: $name; 571 | $options['morphType'] = $annotation->morphType; 572 | $options['morphId'] = $annotation->morphId; 573 | $options['localKey'] = $annotation->localKey ?: 'id'; 574 | } 575 | 576 | // morphOne relation 577 | if ($annotation->type == 'morphOne') { 578 | $options['morphName'] = $annotation->morphName ?: $name; 579 | $options['morphType'] = $annotation->morphType; 580 | $options['morphId'] = $annotation->morphId; 581 | $options['localKey'] = $annotation->localKey ?: 'id'; 582 | } 583 | 584 | // morphTo relation 585 | if ($annotation->type == 'morphTo') { 586 | $options['morphName'] = $annotation->morphName ?: $name; 587 | $options['morphType'] = $annotation->morphType; 588 | $options['morphId'] = $annotation->morphId; 589 | } 590 | 591 | // morphToMany relation 592 | if ($annotation->type == 'morphToMany') { 593 | $options['morphName'] = $annotation->morphName ?: $name; 594 | $options['pivotTable'] = $annotation->pivotTable ?: $this->generatePivotTablename($entityMetadata['class'], $annotation->relatedEntity, $annotation->inverse, $annotation->morphName); 595 | if ($annotation->inverse) { 596 | $options['localPivotKey'] = $annotation->localPivotKey ?: $this->generateKey($entityMetadata['class']); 597 | $options['relatedPivotKey'] = $annotation->morphName.'_id'; 598 | } else { 599 | $options['localPivotKey'] = $annotation->morphName.'_id'; 600 | $options['relatedPivotKey'] = $annotation->relatedPivotKey ?: $this->generateKey($annotation->relatedEntity); 601 | } 602 | $options['inverse'] = $annotation->inverse; 603 | } 604 | 605 | return $options; 606 | } 607 | 608 | /** 609 | * Generate extra columns for a belongsTo relation. 610 | * 611 | * @param string $name 612 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 613 | * @param \ProAI\Datamapper\Metadata\Definitions\Class $entityMetadata 614 | * @return void 615 | */ 616 | protected function generateBelongsToColumns($name, Annotation $annotation, EntityDefinition &$entityMetadata) 617 | { 618 | $relatedForeignKey = $annotation->relatedForeignKey ?: $this->generateKey($annotation->relatedEntity); 619 | 620 | $entityMetadata['table']['columns'][] = $this->getModifiedPrimaryKeyColumn($entityMetadata['table'], [ 621 | 'name' => $relatedForeignKey, 622 | 'primary' => false, 623 | 'options' => [ 624 | 'autoIncrement' => false 625 | ] 626 | ]); 627 | } 628 | 629 | /** 630 | * Generate extra columns for a morphTo relation. 631 | * 632 | * @param array $name 633 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 634 | * @param \ProAI\Datamapper\Metadata\Definitions\Class $entityMetadata 635 | * @return void 636 | */ 637 | protected function generateMorphToColumns($name, Annotation $annotation, EntityDefinition &$entityMetadata) 638 | { 639 | $morphName = (! empty($annotation->morphName)) 640 | ? $annotation->morphName 641 | : $name; 642 | 643 | $morphId = (! empty($annotation->morphId)) 644 | ? $annotation->morphId 645 | : $morphName.'_id'; 646 | 647 | $morphType = (! empty($annotation->morphType)) 648 | ? $annotation->morphType 649 | : $morphName.'_type'; 650 | 651 | $entityMetadata['table']['columns'][] = $this->getModifiedPrimaryKeyColumn($entityMetadata['table'], [ 652 | 'name' => $morphId, 653 | 'primary' => false, 654 | 'options' => [ 655 | 'autoIncrement' => false 656 | ] 657 | ]); 658 | 659 | $entityMetadata['table']['columns'][] = new ColumnDefinition([ 660 | 'name' => $morphType, 661 | 'type' => 'string', 662 | 'nullable' => false, 663 | 'default' => false, 664 | 'primary' => false, 665 | 'unique' => false, 666 | 'index' => false, 667 | 'options' => [] 668 | ]); 669 | } 670 | 671 | /** 672 | * Generate pivot table for a belongsToMany relation. 673 | * 674 | * @param string $name 675 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 676 | * @param \ProAI\Datamapper\Metadata\Definitions\Class $entityMetadata 677 | * @return \ProAI\Datamapper\Metadata\Definitions\Table 678 | */ 679 | protected function generateBelongsToManyPivotTable($name, Annotation $annotation, EntityDefinition &$entityMetadata) 680 | { 681 | $tableName = ($annotation->pivotTable) 682 | ? $annotation->pivotTable 683 | : $this->generatePivotTablename($entityMetadata['class'], $annotation->relatedEntity, $annotation->inverse); 684 | 685 | $localPivotKey = $annotation->localForeignKey ?: $this->generateKey($entityMetadata['class']); 686 | 687 | $relatedPivotKey = $annotation->relatedForeignKey ?: $this->generateKey($annotation->relatedEntity); 688 | 689 | return new TableDefinition([ 690 | 'name' => $tableName, 691 | 'columns' => [ 692 | $this->getModifiedPrimaryKeyColumn($entityMetadata['table'], [ 693 | 'name' => $localPivotKey, 694 | 'options' => [ 695 | 'autoIncrement' => false 696 | ] 697 | ]), 698 | $this->getModifiedPrimaryKeyColumn($entityMetadata['table'], [ 699 | 'name' => $relatedPivotKey, 700 | 'options' => [ 701 | 'autoIncrement' => false 702 | ] 703 | ]), 704 | ] 705 | ]); 706 | } 707 | 708 | /** 709 | * Generate pivot table for a morphToMany relation. 710 | * 711 | * @param string $name 712 | * @param \ProAI\Datamapper\Annotations\Annotation $annotation 713 | * @param \ProAI\Datamapper\Metadata\Definitions\Class $entityMetadata 714 | * @return \ProAI\Datamapper\Metadata\Definitions\Table 715 | */ 716 | protected function generateMorphToManyPivotTable($name, Annotation $annotation, EntityDefinition &$entityMetadata) 717 | { 718 | $morphName = $annotation->morphName; 719 | 720 | $tableName = ($annotation->pivotTable) 721 | ? $annotation->pivotTable 722 | : $this->generatePivotTablename($entityMetadata['class'], $annotation->relatedEntity, $annotation->inverse, $morphName); 723 | 724 | if ($annotation->inverse) { 725 | $pivotKey = $annotation->localPivotKey ?: $this->generateKey($entityMetadata['class']); 726 | } else { 727 | $pivotKey = $annotation->relatedPivotKey ?: $this->generateKey($annotation->relatedEntity); 728 | } 729 | 730 | $morphId = (! empty($annotation->localKey)) 731 | ? $annotation->localKey 732 | : $morphName.'_id'; 733 | 734 | $morphType = $morphName.'_type'; 735 | 736 | return new TableDefinition([ 737 | 'name' => $tableName, 738 | 'columns' => [ 739 | $this->getModifiedPrimaryKeyColumn($entityMetadata['table'], [ 740 | 'name' => $pivotKey, 741 | 'options' => [ 742 | 'autoIncrement' => false 743 | ] 744 | ]), 745 | $this->getModifiedPrimaryKeyColumn($entityMetadata['table'], [ 746 | 'name' => $morphId, 747 | 'options' => [ 748 | 'autoIncrement' => false 749 | ] 750 | ]), 751 | new ColumnDefinition([ 752 | 'name' => $morphType, 753 | 'type' => 'string', 754 | 'nullable' => false, 755 | 'default' => false, 756 | 'primary' => true, 757 | 'unique' => false, 758 | 'index' => false, 759 | 'options' => [] 760 | ]), 761 | ] 762 | ]); 763 | } 764 | 765 | /** 766 | * Get primary key column. 767 | * 768 | * @param \ProAI\Datamapper\Metadata\Definitions\Table $tableMetadata 769 | * @param array $data 770 | * @return \ProAI\Datamapper\Metadata\Definitions\Column 771 | */ 772 | protected function getModifiedPrimaryKeyColumn(TableDefinition $tableMetadata, array $data) 773 | { 774 | foreach($tableMetadata['columns'] as $columnMetadata) { 775 | if ($columnMetadata['primary']) { 776 | $modifiedColumnMetadata = clone $columnMetadata; 777 | foreach($data as $key => $value) { 778 | if ($key == 'options') { 779 | $modifiedColumnMetadata[$key] = array_merge($modifiedColumnMetadata[$key], $value); 780 | } else { 781 | $modifiedColumnMetadata[$key] = $value; 782 | } 783 | } 784 | return $modifiedColumnMetadata; 785 | } 786 | } 787 | return false; 788 | } 789 | 790 | /** 791 | * Generate array of morphable classes for a morphTo or morphToMany relation. 792 | * 793 | * @param array $array 794 | * @return void 795 | */ 796 | protected function generateMorphableClasses(array &$metadata) 797 | { 798 | foreach ($metadata as $key => $entityMetadata) { 799 | foreach ($entityMetadata['relations'] as $relationKey => $relationMetadata) { 800 | // get morphable classes for morphTo relations 801 | if ($relationMetadata['type'] == 'morphTo') { 802 | $metadata[$key]['relations'][$relationKey]['options']['morphableClasses'] 803 | = $this->getMorphableClasses($entityMetadata['class'], $relationMetadata['options']['morphName'], $metadata); 804 | } 805 | 806 | // get morphable classes for morphToMany relations 807 | if ($relationMetadata['type'] == 'morphToMany' && ! $relationMetadata['options']['inverse']) { 808 | $metadata[$key]['relations'][$relationKey]['options']['morphableClasses'] 809 | = $this->getMorphableClasses($entityMetadata['class'], $relationMetadata['options']['morphName'], $metadata, true); 810 | } 811 | } 812 | } 813 | } 814 | 815 | /** 816 | * Get array of morphable classes for a morphTo or morphToMany relation. 817 | * 818 | * @param string $relatedEntity 819 | * @param string $morphName 820 | * @param array $metadata 821 | * @param boolean $many 822 | * @return void 823 | */ 824 | protected function getMorphableClasses($relatedEntity, $morphName, array $metadata, $many=false) 825 | { 826 | $morphableClasses = []; 827 | 828 | foreach ($metadata as $entityMetadata) { 829 | foreach ($entityMetadata['relations'] as $relationMetadata) { 830 | // check relation type 831 | if (! ((! $many && $relationMetadata['type'] == 'morphOne') 832 | || (! $many && $relationMetadata['type'] == 'morphMany') 833 | || ($many && $relationMetadata['type'] == 'morphToMany' && $relationMetadata['options']['inverse']))) { 834 | continue; 835 | } 836 | 837 | // check foreign entity and morph name 838 | if ($relationMetadata['relatedEntity'] == $relatedEntity 839 | && $relationMetadata['options']['morphName'] == $morphName) { 840 | $morphableClasses[$entityMetadata['morphClass']] = $entityMetadata['class']; 841 | } 842 | } 843 | } 844 | 845 | return $morphableClasses; 846 | } 847 | 848 | /** 849 | * Generate a database key based on given key and class. 850 | * 851 | * @param string $class 852 | * @return string 853 | */ 854 | protected function generateKey($class) 855 | { 856 | return snake_case(class_basename($class)).'_id'; 857 | } 858 | 859 | /** 860 | * Generate the database tablename of a pivot table. 861 | * 862 | * @param string $class2 863 | * @param string $class1 864 | * @param boolean $inverse 865 | * @param string $morph 866 | * @return string 867 | */ 868 | protected function generatePivotTablename($class1, $class2, $inverse, $morph=null) 869 | { 870 | // datamapper namespace tables 871 | if ($this->namespaceTablenames) { 872 | $base = ($inverse) 873 | ? $this->generateTableName($class1, true) 874 | : $this->generateTableName($class2, true); 875 | 876 | $related = (! empty($morph)) 877 | ? $morph 878 | : (! empty($inverse) 879 | ? snake_case(class_basename($class2)) 880 | : snake_case(class_basename($class1))); 881 | 882 | return $base . '_' . $related . '_pivot'; 883 | } 884 | 885 | // eloquent default 886 | $base = snake_case(class_basename($class1)); 887 | 888 | $related = snake_case(class_basename($class2)); 889 | 890 | $models = array($related, $base); 891 | 892 | sort($models); 893 | 894 | return strtolower(implode('_', $models)); 895 | } 896 | 897 | /** 898 | * Generate the table associated with the model. 899 | * 900 | * @param string $class 901 | * @return string 902 | */ 903 | protected function generateTableName($class) 904 | { 905 | // datamapper namespace tables 906 | if ($this->namespaceTablenames) { 907 | $className = array_slice(explode('/', str_replace('\\', '/', $class)), 2); 908 | 909 | // delete last entry if entry is equal to the next to last entry 910 | if (count($className) >= 2 && end($className) == prev($className)) { 911 | array_pop($className); 912 | } 913 | 914 | $classBasename = array_pop($className); 915 | 916 | return strtolower(implode('_', array_merge($className, preg_split('/(?<=\\w)(?=[A-Z])/', $classBasename)))); 917 | } 918 | 919 | // eloquent default 920 | return str_replace('\\', '', snake_case(str_plural(class_basename($class)))); 921 | } 922 | 923 | /** 924 | * Generate the class name for polymorphic relations. 925 | * 926 | * @param string $class 927 | * @return string 928 | */ 929 | protected function generateMorphClass($class) 930 | { 931 | // datamapper morphclass abbreviations 932 | if ($this->morphClassAbbreviations) { 933 | return snake_case(class_basename($class)); 934 | } 935 | 936 | // eloquent default 937 | return $class; 938 | } 939 | 940 | /** 941 | * Get snake case version of a name. 942 | * 943 | * @param string $class 944 | * @param string $definedClass 945 | * @return string 946 | */ 947 | protected function getRealEntity($class, $definedClass) 948 | { 949 | $this->validator->validateClass($class, $definedClass); 950 | 951 | return get_real_entity($class); 952 | } 953 | 954 | /** 955 | * Get sanitized version of a name. 956 | * 957 | * @param string $name 958 | * @param string $definedClass 959 | * @return string 960 | */ 961 | protected function getSanitizedName($name, $definedClass) 962 | { 963 | $this->validator->validateName($name, $definedClass); 964 | 965 | return $name; 966 | } 967 | 968 | /** 969 | * Get column name of a name. 970 | * 971 | * @param string $name 972 | * @param boolean $prefix 973 | * @return string 974 | */ 975 | protected function getColumnName($name, $prefix = false) 976 | { 977 | $name = snake_case($name); 978 | 979 | if ($prefix) { 980 | $name = $prefix.'_'.$name; 981 | } 982 | 983 | return $name; 984 | } 985 | } 986 | -------------------------------------------------------------------------------- /src/Metadata/EntityValidator.php: -------------------------------------------------------------------------------- 1 | columnTypes)) { 113 | throw new UnexpectedValueException('Attribute type "'.$type.'" is not supported.'); 114 | } 115 | } 116 | 117 | /** 118 | * Validate a relation type. 119 | * 120 | * @param string $type 121 | * @return void 122 | */ 123 | public function validateRelationType($type) 124 | { 125 | if (! in_array($type, $this->relationTypes)) { 126 | throw new UnexpectedValueException('Relation type "'.$type.'" is not supported.'); 127 | } 128 | } 129 | 130 | /** 131 | * Validate the number of primary keys. 132 | * 133 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 134 | * @return void 135 | */ 136 | public function validatePrimaryKey(EntityDefinition $entityMetadata) 137 | { 138 | // check if all tables have exactly one primary key 139 | $countPrimaryKeys = 0; 140 | 141 | foreach ($entityMetadata['table']['columns'] as $column) { 142 | if (! empty($column['primary'])) { 143 | $countPrimaryKeys++; 144 | } 145 | } 146 | 147 | if ($countPrimaryKeys == 0) { 148 | throw new DomainException('No primary key defined in class ' . $entityMetadata['class'] . '.'); 149 | } elseif ($countPrimaryKeys > 1) { 150 | throw new DomainException('No composite primary keys allowed for class ' . $entityMetadata['class'] . '.'); 151 | } 152 | } 153 | 154 | /** 155 | * Validate the timestamps columns. 156 | * 157 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 158 | * @return void 159 | */ 160 | public function validateTimestamps(EntityDefinition $entityMetadata) 161 | { 162 | $columnNames = []; 163 | 164 | // get column names 165 | foreach ($entityMetadata['table']['columns'] as $column) { 166 | $columnNames[] = $column['name']; 167 | } 168 | 169 | // get version column names 170 | if (! empty($entityMetadata['versionTable'])) { 171 | foreach ($entityMetadata['versionTable']['columns'] as $column) { 172 | $columnNames[] = $column['name']; 173 | } 174 | } 175 | 176 | if (! in_array('created_at', $columnNames) || ! in_array('updated_at', $columnNames)) { 177 | throw new DomainException('@Timestamps annotation defined in class ' . $entityMetadata['class'] . ' requires a $createdAt and an $updatedAt column property.'); 178 | } 179 | } 180 | 181 | /** 182 | * Validate the softdeletes column. 183 | * 184 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 185 | * @return void 186 | */ 187 | public function validateSoftDeletes(EntityDefinition $entityMetadata) 188 | { 189 | $columnNames = []; 190 | 191 | // get column names 192 | foreach ($entityMetadata['table']['columns'] as $column) { 193 | $columnNames[] = $column['name']; 194 | } 195 | 196 | // get version column names 197 | if (! empty($entityMetadata['versionTable'])) { 198 | foreach ($entityMetadata['versionTable']['columns'] as $column) { 199 | $columnNames[] = $column['name']; 200 | } 201 | } 202 | 203 | if (! in_array('deleted_at', $columnNames)) { 204 | throw new DomainException('@SoftDeletes annotation defined in class ' . $entityMetadata['class'] . ' requires a $deletedAt column property.'); 205 | } 206 | } 207 | 208 | /** 209 | * Validate the version table. 210 | * 211 | * @param \ProAI\Datamapper\Metadata\Definitions\Entity $entityMetadata 212 | * @return void 213 | */ 214 | public function validateVersionTable(EntityDefinition $entityMetadata) 215 | { 216 | $columnNames = []; 217 | $countPrimaryKeys = 0; 218 | $versionPrimaryKey = false; 219 | 220 | // get column names 221 | foreach ($entityMetadata['table']['columns'] as $column) { 222 | $columnNames[] = $column['name']; 223 | } 224 | 225 | if (! in_array('latest_version', $columnNames)) { 226 | throw new DomainException('@Versionable annotation defined in class ' . $entityMetadata['class'] . ' requires a $latestVersion column property.'); 227 | } 228 | 229 | $columnNames = []; 230 | 231 | // get version column names 232 | foreach ($entityMetadata['versionTable']['columns'] as $column) { 233 | $columnNames[] = $column['name']; 234 | if (! empty($column['primary'])) { 235 | $countPrimaryKeys++; 236 | } 237 | if (! empty($column['primary']) && $column['name'] == 'version') { 238 | $versionPrimaryKey = true; 239 | } 240 | } 241 | 242 | if (! in_array('version', $columnNames) || ! $versionPrimaryKey) { 243 | throw new DomainException('@Versionable annotation defined in class ' . $entityMetadata['class'] . ' requires a $version property column, which is a primary key.'); 244 | } 245 | if ($countPrimaryKeys > 2) { 246 | throw new DomainException('No more than 2 primary keys are allowed for version table in class ' . $entityMetadata['class'] . '.'); 247 | } 248 | } 249 | 250 | /** 251 | * Check if pivot tables of bi-directional relations are identically. 252 | * 253 | * @param array $metadata 254 | * @return void 255 | */ 256 | public function validatePivotTables($metadata) 257 | { 258 | $pivotTables = []; 259 | foreach ($metadata as $entityMetadata) { 260 | foreach ($entityMetadata['relations'] as $relationMetadata) { 261 | if (! empty($relationMetadata['pivotTable'])) { 262 | $pivotTables[$entityMetadata['class'].$relationMetadata['relatedEntity']] = $relationMetadata; 263 | 264 | if (isset($pivotTables[$relationMetadata['relatedEntity'].$entityMetadata['class']])) { 265 | $relation1 = $pivotTables[$relationMetadata['relatedEntity'].$entityMetadata['class']]; 266 | $relation2 = $relationMetadata; 267 | 268 | $error = null; 269 | 270 | // check name 271 | if ($relation1['pivotTable']['name'] != $relation2['pivotTable']['name']) { 272 | $error = 'Different table names (compared '.$relation1['pivotTable']['name'].' with '.$relation2['pivotTable']['name'].').'; 273 | } 274 | 275 | // check name 276 | if (! empty(array_diff_key($relation1['pivotTable']['columns'], $relation2['pivotTable']['columns']))) { 277 | $columns1 = implode(', ', array_keys($relation1['pivotTable']['columns'])); 278 | $columns2 = implode(', ', array_keys($relation2['pivotTable']['columns'])); 279 | $error = 'Different column names (compared '.$columns1.' with '.$columns2.').'; 280 | } 281 | 282 | if ($error) { 283 | throw new DomainException('Error syncing pivot tables for relations "'.$relation1['name'].'" in "'.$relation2['relatedEntity'].'" and "'.$relation2['name'].'" in "'.$relation1['relatedEntity'].'": '.$error); 284 | } 285 | } 286 | } 287 | } 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/Providers/BaseServiceProvider.php: -------------------------------------------------------------------------------- 1 | app['config']['datamapper.auto_scan']) { 18 | $this->scanEntities(); 19 | } 20 | 21 | $this->registerEloquentModels(); 22 | } 23 | 24 | /** 25 | * Register the application services. 26 | * 27 | * @return void 28 | */ 29 | public function register() 30 | { 31 | $this->registerEntityManager(); 32 | 33 | $this->registerHelpers(); 34 | 35 | $this->app->register('ProAI\Datamapper\Providers\CommandsServiceProvider'); 36 | 37 | } 38 | 39 | /** 40 | * Register the entity manager implementation. 41 | * 42 | * @return void 43 | */ 44 | protected function registerEntityManager() 45 | { 46 | $app = $this->app; 47 | 48 | $app->singleton('datamapper.entitymanager', function ($app) { 49 | return new EntityManager; 50 | }); 51 | } 52 | 53 | /** 54 | * Register helpers. 55 | * 56 | * @return void 57 | */ 58 | protected function registerHelpers() 59 | { 60 | require_once __DIR__ . '/../Support/helpers.php'; 61 | } 62 | 63 | /** 64 | * Scan entity annotations and update database. 65 | * 66 | * @return void 67 | */ 68 | protected function scanEntities() 69 | { 70 | $app = $this->app; 71 | 72 | // get classes 73 | $classes = $app['datamapper.classfinder']->getClassesFromNamespace($app['config']['datamapper.models_namespace']); 74 | 75 | // build metadata 76 | $metadata = $app['datamapper.entity.scanner']->scan($classes, $app['config']['datamapper.namespace_tablenames'], $app['config']['datamapper.morphclass_abbreviations']); 77 | 78 | // generate eloquent models 79 | $app['datamapper.eloquent.generator']->generate($metadata, false); 80 | 81 | // build schema 82 | $app['datamapper.schema.builder']->update($metadata, false); 83 | } 84 | 85 | /** 86 | * Load the compiled eloquent entity models. 87 | * 88 | * @return void 89 | */ 90 | protected function registerEloquentModels() 91 | { 92 | $files = $this->app['files']->files(storage_path('framework/entities')); 93 | 94 | foreach ($files as $file) { 95 | if ($this->app['files']->extension($file) == '') { 96 | require_once $file; 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Providers/CommandsServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->register('ProAI\Datamapper\Providers\MetadataServiceProvider'); 31 | 32 | $this->registerEntityScanner(); 33 | 34 | $this->registerSchemaBuilder(); 35 | 36 | $this->registerModelGenerator(); 37 | 38 | $this->registerCommands(); 39 | } 40 | 41 | /** 42 | * Register the entity scanner implementation. 43 | * 44 | * @return void 45 | */ 46 | protected function registerEntityScanner() 47 | { 48 | $app = $this->app; 49 | 50 | $app->singleton('datamapper.entity.scanner', function ($app) { 51 | $reader = $app['datamapper.annotationreader']; 52 | 53 | $validator = new EntityValidator; 54 | 55 | return new EntityScanner($reader, $validator); 56 | }); 57 | } 58 | 59 | /** 60 | * Register the scehma builder implementation. 61 | * 62 | * @return void 63 | */ 64 | protected function registerSchemaBuilder() 65 | { 66 | $app = $this->app; 67 | 68 | $app->singleton('datamapper.schema.builder', function ($app) { 69 | $connection = $app['db']->connection(); 70 | 71 | return new SchemaBuilder($connection); 72 | }); 73 | } 74 | 75 | /** 76 | * Register the scehma builder implementation. 77 | * 78 | * @return void 79 | */ 80 | protected function registerModelGenerator() 81 | { 82 | $app = $this->app; 83 | 84 | $app->singleton('datamapper.eloquent.generator', function ($app) { 85 | $path = storage_path('framework/entities'); 86 | 87 | return new ModelGenerator($app['files'], $path); 88 | }); 89 | } 90 | 91 | /** 92 | * Register all of the migration commands. 93 | * 94 | * @return void 95 | */ 96 | protected function registerCommands() 97 | { 98 | // create singletons of each command 99 | $commands = array('Create', 'Update', 'Drop'); 100 | 101 | foreach ($commands as $command) { 102 | $this->{'register'.$command.'Command'}(); 103 | } 104 | 105 | // register commands 106 | $this->commands( 107 | 'command.schema.create', 108 | 'command.schema.update', 109 | 'command.schema.drop' 110 | ); 111 | } 112 | 113 | /** 114 | * Register the "schema:create" command. 115 | * 116 | * @return void 117 | */ 118 | protected function registerCreateCommand() 119 | { 120 | $this->app->singleton('command.schema.create', function ($app) { 121 | return new SchemaCreateCommand( 122 | $app['datamapper.classfinder'], 123 | $app['datamapper.entity.scanner'], 124 | $app['datamapper.schema.builder'], 125 | $app['datamapper.eloquent.generator'], 126 | $app['config']['datamapper'] 127 | ); 128 | }); 129 | } 130 | 131 | /** 132 | * Register the "schema:update" command. 133 | * 134 | * @return void 135 | */ 136 | protected function registerUpdateCommand() 137 | { 138 | $this->app->singleton('command.schema.update', function ($app) { 139 | return new SchemaUpdateCommand( 140 | $app['datamapper.classfinder'], 141 | $app['datamapper.entity.scanner'], 142 | $app['datamapper.schema.builder'], 143 | $app['datamapper.eloquent.generator'], 144 | $app['config']['datamapper'] 145 | ); 146 | }); 147 | } 148 | 149 | /** 150 | * Register the "schema:drop" command. 151 | * 152 | * @return void 153 | */ 154 | protected function registerDropCommand() 155 | { 156 | $this->app->singleton('command.schema.drop', function ($app) { 157 | return new SchemaDropCommand( 158 | $app['datamapper.classfinder'], 159 | $app['datamapper.entity.scanner'], 160 | $app['datamapper.schema.builder'], 161 | $app['datamapper.eloquent.generator'], 162 | $app['config']['datamapper'] 163 | ); 164 | }); 165 | } 166 | 167 | /** 168 | * Get the services provided by the provider. 169 | * 170 | * @return array 171 | */ 172 | public function provides() 173 | { 174 | return [ 175 | 'datamapper.entity.scanner', 176 | 'datamapper.schema.builder', 177 | 'datamapper.eloquent.generator', 178 | 'command.schema.create', 179 | 'command.schema.update', 180 | 'command.schema.drop', 181 | ]; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Providers/LumenServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerConfig(); 17 | 18 | parent::register(); 19 | } 20 | 21 | /** 22 | * Register the config. 23 | * 24 | * @return void 25 | */ 26 | protected function registerConfig() 27 | { 28 | $this->app->configure('datamapper'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Providers/MetadataServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerAnnotations(); 28 | 29 | $this->registerAnnotationReader(); 30 | 31 | $this->registerClassFinder(); 32 | } 33 | 34 | /** 35 | * Registers all annotation classes 36 | * 37 | * @return void 38 | */ 39 | public function registerAnnotations() 40 | { 41 | $app = $this->app; 42 | 43 | $loader = new AnnotationLoader($app['files'], __DIR__ . '/../Annotations'); 44 | 45 | $loader->registerAll(); 46 | } 47 | 48 | /** 49 | * Register the class finder implementation. 50 | * 51 | * @return void 52 | */ 53 | protected function registerAnnotationReader() 54 | { 55 | $this->app->singleton('datamapper.annotationreader', function ($app) { 56 | return new AnnotationReader; 57 | }); 58 | } 59 | 60 | /** 61 | * Register the class finder implementation. 62 | * 63 | * @return void 64 | */ 65 | protected function registerClassFinder() 66 | { 67 | $this->app->singleton('datamapper.classfinder', function ($app) { 68 | $finder = new FilesystemClassFinder; 69 | 70 | return new ClassFinder($finder); 71 | }); 72 | } 73 | 74 | /** 75 | * Get the services provided by the provider. 76 | * 77 | * @return array 78 | */ 79 | public function provides() 80 | { 81 | return [ 82 | 'datamapper.classfinder', 83 | 'datamapper.annotationreader', 84 | ]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Schema/Builder.php: -------------------------------------------------------------------------------- 1 | ['type' => 'bigint'], 22 | 'smallInteger' => ['type' => 'smallint'], 23 | // chars 24 | 'char' => ['type' => 'string', 'options' => ['fixed' => true]], 25 | // texts 26 | 'text' => ['type' => 'text', 'options' => ['length' => 65535]], 27 | 'mediumText' => ['type' => 'text', 'options' => ['length' => 16777215]], 28 | 'longText' => ['type' => 'text', 'options' => ['length' => 4294967295]], 29 | // timestamps 30 | 'dateTime' => ['type' => 'datetime', 'options' => ['default' => '0000-00-00 00:00:00']], 31 | //'timestamp' => ['type' => 'datetime', 'options' => ['default' => '0000-00-00 00:00:00', 'platformOptions' => ['version' => true]]], 32 | ]; 33 | 34 | /** 35 | * The database connection instance. 36 | * 37 | * @var \Illuminate\Database\Connection 38 | */ 39 | protected $connection; 40 | 41 | /** 42 | * Doctrine DBAL Schema Manager instance. 43 | * 44 | * @var \Doctrine\DBAL\Schema\AbstractSchemaManager 45 | */ 46 | protected $schemaManager; 47 | 48 | /** 49 | * Doctrine DBAL database platform instance. 50 | * 51 | * @var \Doctrine\DBAL\Platforms\AbstractPlatform 52 | */ 53 | protected $platform; 54 | 55 | /** 56 | * Constructor. 57 | * 58 | * @param \Illuminate\Database\Connection $connection 59 | * @return void 60 | */ 61 | public function __construct(Connection $connection) 62 | { 63 | $this->connection = $connection; 64 | $this->schemaManager = $connection->getDoctrineSchemaManager(); 65 | $this->platform = $this->schemaManager->getDatabasePlatform(); 66 | } 67 | 68 | /** 69 | * Create all tables. 70 | * 71 | * @param array $metadata 72 | * @return array 73 | */ 74 | public function create(array $metadata) 75 | { 76 | $schema = $this->getSchemaFromMetadata($metadata); 77 | 78 | $statements = $schema->toSql($this->platform); 79 | 80 | $this->build($statements); 81 | 82 | return $statements; 83 | } 84 | 85 | /** 86 | * Update all tables. 87 | * 88 | * @param array $metadata 89 | * @param boolean $saveMode 90 | * @return array 91 | */ 92 | public function update(array $metadata, $saveMode=false) 93 | { 94 | $fromSchema = $this->schemaManager->createSchema(); 95 | $toSchema = $this->getSchemaFromMetadata($metadata); 96 | 97 | $comparator = new Comparator; 98 | $schemaDiff = $comparator->compare($fromSchema, $toSchema); 99 | 100 | if ($saveMode) { 101 | $statements = $schemaDiff->toSaveSql($this->platform); 102 | } else { 103 | $statements = $schemaDiff->toSql($this->platform); 104 | } 105 | 106 | $this->build($statements); 107 | 108 | return $statements; 109 | } 110 | 111 | /** 112 | * Drop all tables. 113 | * 114 | * @param array $metadata 115 | * @return array 116 | */ 117 | public function drop(array $metadata) 118 | { 119 | $visitor = new DropSchemaSqlCollector($this->platform); 120 | 121 | $schema = $this->getSchemaFromMetadata($metadata); 122 | 123 | $fullSchema = $this->schemaManager->createSchema(); 124 | 125 | foreach ($fullSchema->getTables() as $table) { 126 | if ($schema->hasTable($table->getName())) { 127 | $visitor->acceptTable($table); 128 | } 129 | } 130 | 131 | $statements = $visitor->getQueries(); 132 | 133 | $this->build($statements); 134 | 135 | return $statements; 136 | } 137 | 138 | /** 139 | * Execute the statements against the database. 140 | * 141 | * @param array $statements 142 | * @return void 143 | */ 144 | protected function build($statements) 145 | { 146 | foreach ($statements as $statement) { 147 | $this->connection->statement($statement); 148 | } 149 | } 150 | 151 | /** 152 | * Create schema instance from metadata 153 | * 154 | * @param array $metadata 155 | * @return \Doctrine\DBAL\Schema\Schema 156 | */ 157 | public function getSchemaFromMetadata(array $metadata) 158 | { 159 | $entityMetadataSchemaConfig = $this->schemaManager->createSchemaConfig(); 160 | $schema = new Schema([], [], $entityMetadataSchemaConfig); 161 | $pivotTables = []; 162 | 163 | foreach ($metadata as $entityMetadata) { 164 | $this->generateTableFromMetadata($schema, $entityMetadata['table']); 165 | // create version table 166 | if (! empty($entityMetadata['versionTable'])) { 167 | $this->generateTableFromMetadata($schema, $entityMetadata['versionTable']); 168 | } 169 | 170 | foreach ($entityMetadata['relations'] as $relationMetadata) { 171 | if (! empty($relationMetadata['pivotTable'])) { 172 | // create pivot table for many to many relations 173 | if (! in_array($relationMetadata['pivotTable']['name'], $pivotTables)) { 174 | $this->generateTableFromMetadata($schema, $relationMetadata['pivotTable']); 175 | } 176 | 177 | $pivotTables[] = $relationMetadata['pivotTable']['name']; 178 | } 179 | } 180 | } 181 | 182 | return $schema; 183 | } 184 | 185 | /** 186 | * Generate a table from metadata. 187 | * 188 | * @param table \Doctrine\DBAL\Schema\Schema 189 | * @param \ProAI\Datamapper\Metadata\Definitions\Table $tableMetadata 190 | * @return void 191 | */ 192 | protected function generateTableFromMetadata($schema, TableDefinition $tableMetadata) 193 | { 194 | $primaryKeys = []; 195 | $uniqueIndexes = []; 196 | $indexes = []; 197 | 198 | $table = $schema->createTable($this->connection->getTablePrefix().$tableMetadata['name']); 199 | 200 | foreach ($tableMetadata['columns'] as $columnMetadata) { 201 | $columnMetadata = $this->getDoctrineColumnAliases($columnMetadata); 202 | 203 | // add column 204 | $options = $this->getDoctrineColumnOptions($columnMetadata); 205 | $table->addColumn($columnMetadata['name'], $columnMetadata['type'], $options); 206 | 207 | // add primary keys, unique indexes and indexes 208 | if (! empty($columnMetadata['primary'])) { 209 | $primaryKeys[] = $columnMetadata['name']; 210 | } 211 | if (! empty($columnMetadata['unique'])) { 212 | $uniqueIndexes[] = $columnMetadata['name']; 213 | } 214 | if (! empty($columnMetadata['index'])) { 215 | $indexes[] = $columnMetadata['name']; 216 | } 217 | } 218 | 219 | // add primary keys, unique indexes and indexes 220 | if (! empty($primaryKeys)) { 221 | $table->setPrimaryKey($primaryKeys); 222 | } 223 | if (! empty($uniqueIndexes)) { 224 | $table->addUniqueIndex($uniqueIndexes); 225 | } 226 | if (! empty($indexes)) { 227 | $table->addIndex($indexes); 228 | } 229 | } 230 | 231 | /** 232 | * Get the doctrine column type. 233 | * 234 | * @param \ProAI\Datamapper\Metadata\Definitions\Column $columnMetadata 235 | * @return array 236 | */ 237 | protected function getDoctrineColumnAliases(ColumnDefinition $columnMetadata) 238 | { 239 | if (in_array($columnMetadata['type'], array_keys($this->aliases))) { 240 | $index = $columnMetadata['type']; 241 | 242 | // fix for nullable datetime 243 | if ($columnMetadata['type'] == 'dateTime' && ! empty($columnMetadata['nullable'])) { 244 | $this->aliases['dateTime']['options'] = array_except($this->aliases['dateTime']['options'], 'default'); 245 | } 246 | 247 | // update primary key 248 | if (! empty($this->aliases[$index]['primary'])) { 249 | $columnMetadata['primary'] = $this->aliases[$index]['primary']; 250 | } 251 | 252 | // update unsigned 253 | if (! empty($this->aliases[$index]['options']['unsigned'])) { 254 | $columnMetadata['unsigned'] = $this->aliases[$index]['options']['unsigned']; 255 | } 256 | 257 | // update options 258 | if (! empty($this->aliases[$index]['options'])) { 259 | $columnMetadata['options'] = array_merge($columnMetadata['options'], $this->aliases[$index]['options']); 260 | } 261 | 262 | // update type 263 | $columnMetadata['type'] = $this->aliases[$index]['type']; 264 | } 265 | 266 | return $columnMetadata; 267 | } 268 | 269 | /** 270 | * Get the doctrine column options. 271 | * 272 | * @param \ProAI\Datamapper\Metadata\Definitions\Column $columnMetadata 273 | * @return array 274 | */ 275 | protected function getDoctrineColumnOptions(ColumnDefinition $columnMetadata) 276 | { 277 | $options = $columnMetadata['options']; 278 | 279 | // alias for nullable option 280 | if (! empty($columnMetadata['nullable'])) { 281 | $options['notnull'] = ! $columnMetadata['nullable']; 282 | } 283 | 284 | // alias for default option 285 | if (! empty($columnMetadata['default'])) { 286 | $options['default'] = $columnMetadata['default']; 287 | } 288 | 289 | // alias for unsigned option 290 | if (! empty($columnMetadata['options']['unsigned'])) { 291 | $options['unsigned'] = $columnMetadata['options']['unsigned']; 292 | } 293 | 294 | // alias for autoincrement option 295 | if (! empty($columnMetadata['options']['autoIncrement'])) { 296 | $options['autoincrement'] = $columnMetadata['options']['autoIncrement']; 297 | } 298 | 299 | // alias for fixed option 300 | if (! empty($columnMetadata['options']['fixed'])) { 301 | $options['fixed'] = $columnMetadata['options']['fixed']; 302 | } 303 | 304 | return $options; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/Support/AggregateRoot.php: -------------------------------------------------------------------------------- 1 | items[] = $item; 18 | 19 | return $this; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Support/DataTransferObject.php: -------------------------------------------------------------------------------- 1 | newInstanceWithoutConstructor(); 24 | 25 | // get model data 26 | $dict = [ 27 | 'mapping' => $eloquentModel->getMapping(), 28 | 'attributes' => $eloquentModel->getAttributes(), 29 | 'relations' => $eloquentModel->getRelations() 30 | ]; 31 | 32 | // attributes 33 | foreach ($dict['mapping']['attributes'] as $attribute => $column) { 34 | $entity->{$attribute} = $dict['attributes'][$column]; 35 | } 36 | 37 | // embeddeds 38 | foreach ($dict['mapping']['embeddeds'] as $name => $embedded) { 39 | $entity->{$name} = $embedded['class']::newFromEloquentObject($eloquentModel, $name); 40 | } 41 | 42 | // relations 43 | foreach ($dict['mapping']['relations'] as $name => $relation) { 44 | // set relation object 45 | if (! empty($dict['relations'][$name])) { 46 | $relationObject = $dict['relations'][$name]->toDatamapperObject(); 47 | } elseif (in_array($relation['type'], $eloquentModel->manyRelations)) { 48 | $relationObject = new ProxyCollection; 49 | } else { 50 | $relationObject = new Proxy; 51 | } 52 | 53 | $entity->{$name} = $relationObject; 54 | } 55 | 56 | return $entity; 57 | } 58 | 59 | /** 60 | * Convert an instance to an eloquent model object. 61 | * 62 | * @param string $lastObjectId 63 | * @param \ProAI\Datamapper\Eloquent\Model $lastEloquentModel 64 | * @return \ProAI\Datamapper\Eloquent\Model 65 | */ 66 | public function toEloquentObject($lastObjectId, $lastEloquentModel) 67 | { 68 | $class = get_mapped_model(static::class); 69 | 70 | $eloquentModel = new $class; 71 | 72 | // get model data 73 | $dict = [ 74 | 'mapping' => $eloquentModel->getMapping(), 75 | 'attributes' => $eloquentModel->getAttributes(), 76 | 'relations' => $eloquentModel->getRelations() 77 | ]; 78 | 79 | // attributes 80 | foreach ($dict['mapping']['attributes'] as $attribute => $column) { 81 | if (! $eloquentModel->isAutomaticallyUpdatedDate($column)) { 82 | $eloquentModel->setAttribute($column, $this->{$attribute}); 83 | } 84 | } 85 | 86 | // embeddeds 87 | foreach ($dict['mapping']['embeddeds'] as $name => $embedded) { 88 | $embeddedObject = $this->{$name}; 89 | 90 | if (! empty($embeddedObject)) { 91 | $embeddedObject->toEloquentObject($eloquentModel, $name); 92 | } 93 | } 94 | 95 | // relations 96 | foreach ($dict['mapping']['relations'] as $name => $relation) { 97 | $relationObject = $this->{$name}; 98 | 99 | if (! empty($relationObject) && ! $relationObject instanceof \ProAI\Datamapper\Contracts\Proxy) { 100 | // set relation 101 | if ($relationObject instanceof \ProAI\Datamapper\Support\Collection) { 102 | $value = EloquentCollection::newFromDatamapperObject($relationObject, $this, $eloquentModel); 103 | } elseif (spl_object_hash($relationObject) == $lastObjectId) { 104 | $value = $lastEloquentModel; 105 | } else { 106 | $value = EloquentModel::newFromDatamapperObject($relationObject, spl_object_hash($this), $eloquentModel); 107 | } 108 | 109 | $eloquentModel->setRelation($name, $value); 110 | } 111 | } 112 | 113 | return $eloquentModel; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Support/Facades/EntityManager.php: -------------------------------------------------------------------------------- 1 | {$method}) || property_exists($this, $method)) && ! is_scalar($this->{$method})) 30 | { 31 | return $this->{$method}; 32 | } 33 | 34 | // magical getter for scalars 35 | $scalarProperty = (substr($method, 0, 3) == 'get') 36 | ? lcfirst(substr($method, 3)) 37 | : null; 38 | 39 | if ($scalarProperty && (isset($this->{$scalarProperty}) || property_exists($this, $scalarProperty))) 40 | { 41 | if (is_scalar($this->{$scalarProperty})) { 42 | return $this->{$scalarProperty}; 43 | } 44 | if ($this->{$scalarProperty} instanceof \ProAI\Datamapper\Support\ValueObject) { 45 | return (string) $this->{$scalarProperty}; 46 | } 47 | } 48 | 49 | $class = get_class($this); 50 | $trace = debug_backtrace(); 51 | $file = $trace[0]['file']; 52 | $line = $trace[0]['line']; 53 | trigger_error("Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Support/Proxy.php: -------------------------------------------------------------------------------- 1 | toArray(), $options); 22 | } 23 | 24 | /** 25 | * Convert the entity instance to an array. 26 | * 27 | * @return array 28 | */ 29 | public function toArray() 30 | { 31 | return null; 32 | } 33 | 34 | /** 35 | * Determine if an item exists at an offset. 36 | * 37 | * @param mixed $offset 38 | * @return bool 39 | */ 40 | public function offsetExists($offset) 41 | { 42 | $this->getException($method); 43 | } 44 | 45 | /** 46 | * Get an item at a given offset. 47 | * 48 | * @param mixed $offset 49 | * @return mixed 50 | */ 51 | public function offsetGet($offset) 52 | { 53 | $this->getException($method); 54 | } 55 | 56 | /** 57 | * Set the item at a given offset. 58 | * 59 | * @param mixed $offset 60 | * @param mixed $value 61 | * @return void 62 | */ 63 | public function offsetSet($offset, $value) 64 | { 65 | $this->getException($method); 66 | } 67 | 68 | /** 69 | * Unset the item at a given offset. 70 | * 71 | * @param string $offset 72 | * @return void 73 | */ 74 | public function offsetUnset($offset) 75 | { 76 | $this->getException($method); 77 | } 78 | 79 | /** 80 | * Proxy exception. 81 | * 82 | * @param string $method 83 | * @return void 84 | */ 85 | public function getException($method) 86 | { 87 | throw new Exception('You tried to call method "'.$method.'" from a proxy object.'); 88 | } 89 | 90 | /** 91 | * Handle dynamic method calls into the model. 92 | * 93 | * @param string $method 94 | * @param array $parameters 95 | * @return mixed 96 | */ 97 | public function __call($method, $parameters) 98 | { 99 | $this->getException($method); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Support/ProxyCollection.php: -------------------------------------------------------------------------------- 1 | deletedAt->date()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Support/Traits/Timestamps.php: -------------------------------------------------------------------------------- 1 | createdAt->date()); 26 | } 27 | 28 | /** 29 | * @return \Carbon\Carbon 30 | */ 31 | public function updatedAt() 32 | { 33 | return Carbon::instance($this->updatedAt->date()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Support/Traits/Versionable.php: -------------------------------------------------------------------------------- 1 | latestVersion->version(); 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | public function version() 33 | { 34 | return $this->version->version(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Support/Traits/VersionableSoftDeletes.php: -------------------------------------------------------------------------------- 1 | deletedAt->date()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Support/Traits/VersionableTimestamps.php: -------------------------------------------------------------------------------- 1 | createdAt->date()); 27 | } 28 | 29 | /** 30 | * @return \Carbon\Carbon 31 | */ 32 | public function updatedAt() 33 | { 34 | return Carbon::instance($this->updatedAt->date()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Support/ValueObject.php: -------------------------------------------------------------------------------- 1 | $value) { 21 | if ($this->{$name} !== $valueObject->{$name}) { 22 | return false; 23 | } 24 | } 25 | 26 | return true; 27 | } 28 | 29 | /** 30 | * Build new instance from an eloquent model object. 31 | * 32 | * @param \Illuminate\Database\Eloquent\Model $eloquentModel 33 | * @param array $name 34 | * @return \ProAI\Datamapper\Support\ValueObject 35 | */ 36 | public static function newFromEloquentObject(EloquentModel $eloquentModel, $name) 37 | { 38 | $rc = new ReflectionClass(static::class); 39 | $valueObject = $rc->newInstanceWithoutConstructor(); 40 | 41 | // get model data 42 | $dict = [ 43 | 'mapping' => $eloquentModel->getMapping(), 44 | 'attributes' => $eloquentModel->getAttributes() 45 | ]; 46 | 47 | foreach ($dict['mapping']['embeddeds'][$name]['attributes'] as $attribute => $column) { 48 | $valueObject->{$attribute} = $dict['attributes'][$column]; 49 | } 50 | 51 | return $valueObject; 52 | } 53 | 54 | /** 55 | * Convert an instance to an eloquent model object. 56 | * 57 | * @param \Illuminate\Database\Eloquent\Model $eloquentModel 58 | * @param array $name 59 | * @return void 60 | */ 61 | public function toEloquentObject(EloquentModel &$eloquentModel, $name) 62 | { 63 | // get model data 64 | $dict = [ 65 | 'mapping' => $eloquentModel->getMapping() 66 | ]; 67 | 68 | foreach ($dict['mapping']['embeddeds'][$name]['attributes'] as $attribute => $column) { 69 | $eloquentModel->setAttribute($column, $this->{$attribute}); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Support/helpers.php: -------------------------------------------------------------------------------- 1 | {{type}}({{options}}); 9 | } -------------------------------------------------------------------------------- /stubs/model.stub: -------------------------------------------------------------------------------- 1 |