├── .editorconfig ├── src ├── MapperAwareInterface.php ├── Relations │ ├── RelationsAwareInterface.php │ ├── RelationInterface.php │ ├── HasOne.php │ ├── RelationTrait.php │ ├── BelongsTo.php │ ├── HasMany.php │ └── ManyToMany.php ├── Query │ ├── QueryAwareInterface.php │ └── Events │ │ ├── AbstractQueryEvent.php │ │ ├── QueryBuilderEvent.php │ │ └── QueryResultEvent.php ├── MapperFactoryInterface.php ├── QueryFactoryInterface.php ├── Entity │ ├── ProviderFactoryInterface.php │ ├── ProviderFactoryTrait.php │ ├── EntityAwareInterface.php │ ├── ProviderInterface.php │ ├── TransformerInterface.php │ ├── EntityAwareTrait.php │ ├── DefinitionInterface.php │ ├── Provider.php │ ├── Transformer.php │ └── Definition.php ├── QueryInterface.php ├── MapperFactoryTrait.php ├── ConnectionAwareInterface.php ├── CacheAwareTrait.php ├── RepositoryInterface.php ├── Hydrator │ ├── HydratorInterface.php │ └── EntityHydrator.php ├── ConnectionAwareTrait.php ├── ConnectionManagerInterface.php ├── EventEmitterFactoryInterface.php ├── GatewayInterface.php ├── MapperInterface.php ├── AbstractRepository.php ├── Support.php ├── EventEmitterFactoryTrait.php ├── Gateway.php ├── Connection.php ├── ConnectionManager.php ├── Mapper.php └── Query.php ├── phpunit.xml ├── LICENSE.md ├── composer.json ├── CONTRIBUTING.md ├── ChangeLog-1.0.md └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in 2 | # this file, please see the EditorConfig documentation: 3 | # http://editorconfig.org/ 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | indent_size = 2 13 | indent_style = space 14 | 15 | 16 | [*.{php,js,html,css,xml,phtml,md,markdown}] 17 | indent_size = 4 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [*.bat] 23 | end_of_line = crlf 24 | -------------------------------------------------------------------------------- /src/MapperAwareInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 01.03.2016 10 | * Time: 10:42 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | interface MapperAwareInterface 18 | { 19 | /** 20 | * Get entity mapper 21 | * 22 | * @return MapperInterface 23 | */ 24 | public function getMapper(); 25 | } 26 | -------------------------------------------------------------------------------- /src/Relations/RelationsAwareInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 01.03.2016 10 | * Time: 09:38 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Relations; 15 | 16 | 17 | use Blast\Orm\Query; 18 | 19 | interface RelationsAwareInterface 20 | { 21 | /** 22 | * @return RelationInterface[] 23 | */ 24 | public function getRelations(); 25 | } 26 | -------------------------------------------------------------------------------- /src/Query/QueryAwareInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 01.03.2016 10 | * Time: 10:43 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Query; 15 | 16 | 17 | use Blast\Orm\Query; 18 | 19 | interface QueryAwareInterface 20 | { 21 | /** 22 | * Get modified query 23 | * 24 | * @return Query 25 | */ 26 | public function getQuery(); 27 | } 28 | -------------------------------------------------------------------------------- /src/MapperFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 16.03.2016 10 | * Time: 17:25 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | interface MapperFactoryInterface 18 | { 19 | /** 20 | * Create a new Mapper for given entity. 21 | * 22 | * @param $entity 23 | * @return Mapper 24 | */ 25 | public function createMapper($entity); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/QueryFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 13.04.2016 10 | * Time: 08:34 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | interface QueryFactoryInterface 18 | { 19 | /** 20 | * Create a new query for given entity. 21 | * 22 | * @param $entity 23 | * 24 | * @return Mapper 25 | */ 26 | public function createQuery($entity = null); 27 | } 28 | -------------------------------------------------------------------------------- /src/Entity/ProviderFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 16.03.2016 10 | * Time: 17:23 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Entity; 15 | 16 | 17 | interface ProviderFactoryInterface 18 | { 19 | /** 20 | * Create a new provider for given entity 21 | * 22 | * @param $entity 23 | * @return Provider 24 | */ 25 | public function createProvider($entity); 26 | } 27 | -------------------------------------------------------------------------------- /src/Entity/ProviderFactoryTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 16.03.2016 10 | * Time: 17:22 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Entity; 15 | 16 | 17 | trait ProviderFactoryTrait 18 | { 19 | 20 | /** 21 | * Create a new provider for given entity 22 | * 23 | * @param $entity 24 | * @return Provider 25 | */ 26 | public function createProvider($entity){ 27 | return new Provider($entity); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Entity/EntityAwareInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 23.02.2016 10 | * Time: 10:20 11 | * 12 | */ 13 | namespace Blast\Orm\Entity; 14 | 15 | interface EntityAwareInterface 16 | { 17 | /** 18 | * @return array|\stdClass|\ArrayObject|object 19 | */ 20 | public function getEntity(); 21 | 22 | /** 23 | * @param array|\ArrayObject|\stdClass|object $entity 24 | * @return $this 25 | */ 26 | public function setEntity($entity); 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | ./tests/Stubs 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/QueryInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 23.02.2016 10 | * Time: 10:19 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | use Blast\Orm\Hydrator\HydratorInterface; 17 | 18 | interface QueryInterface 19 | { 20 | /** 21 | * Fetch data for entity 22 | * 23 | * @param string $option 24 | * @return array|\SplStack|\ArrayObject|object 25 | * @throws \Doctrine\DBAL\DBALException 26 | */ 27 | public function execute($option = HydratorInterface::HYDRATE_AUTO); 28 | } 29 | -------------------------------------------------------------------------------- /src/MapperFactoryTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 16.03.2016 10 | * Time: 17:24 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | trait MapperFactoryTrait 18 | { 19 | /** 20 | * Create a new Mapper for given entity or provider and 21 | * pass additional database connection. 22 | * 23 | * @param $entity 24 | * @param null $connection 25 | * @return Mapper 26 | */ 27 | public function createMapper($entity, $connection = null){ 28 | return new Mapper($entity, $connection); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ConnectionAwareInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 15.03.2016 10 | * Time: 20:33 11 | */ 12 | 13 | namespace Blast\Orm; 14 | 15 | 16 | interface ConnectionAwareInterface 17 | { 18 | /** 19 | * Get current connection 20 | * 21 | * @return \Doctrine\DBAL\Driver\Connection|\Doctrine\DBAL\Connection 22 | */ 23 | public function getConnection(); 24 | 25 | /** 26 | * @param \Doctrine\DBAL\Driver\Connection|null $connection 27 | * 28 | * @return $this 29 | */ 30 | public function setConnection($connection); 31 | } 32 | -------------------------------------------------------------------------------- /src/Entity/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 07.03.2016 10 | * Time: 12:03 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Entity; 15 | 16 | 17 | use Blast\Orm\MapperAwareInterface; 18 | use Blast\Orm\Relations\RelationsAwareInterface; 19 | use Doctrine\DBAL\Schema\Column; 20 | use Doctrine\DBAL\Schema\Index; 21 | 22 | interface ProviderInterface extends EntityAwareInterface 23 | { 24 | 25 | const DEFAULT_PRIMARY_KEY_NAME = 'id'; 26 | 27 | /** 28 | * @return DefinitionInterface 29 | */ 30 | public function getDefinition(); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/CacheAwareTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 27.07.2016 10 | * Time: 10:22 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | trait CacheAwareTrait 18 | { 19 | 20 | /** 21 | * @return \Doctrine\Common\Cache\Cache 22 | */ 23 | public function getMetaDataCache(){ 24 | return ConnectionManager::getInstance()->get()->getMetaDataCache(); 25 | } 26 | 27 | /** 28 | * @return \Doctrine\Common\Cache\Cache 29 | */ 30 | public function getReflectionCache(){ 31 | return ConnectionManager::getInstance()->get()->getReflectionCache(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Relations/RelationInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 29.02.2016 10 | * Time: 12:07 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Relations; 15 | 16 | use Blast\Orm\Entity\EntityAwareInterface; 17 | use Blast\Orm\Query; 18 | 19 | interface RelationInterface extends EntityAwareInterface 20 | { 21 | /** 22 | * @return Query 23 | */ 24 | public function getQuery(); 25 | 26 | /** 27 | * Name of relation 28 | * 29 | * @return string 30 | */ 31 | public function getName(); 32 | 33 | /** 34 | * @return array|\ArrayObject||bool 35 | */ 36 | public function execute(); 37 | } 38 | -------------------------------------------------------------------------------- /src/Relations/HasOne.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 29.02.2016 10 | * Time: 16:37 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Relations; 15 | 16 | 17 | use Blast\Orm\Hydrator\HydratorInterface; 18 | 19 | class HasOne extends HasMany 20 | { 21 | /** 22 | * @return \ArrayObject|object 23 | */ 24 | public function execute() 25 | { 26 | return $this->getQuery()->execute(HydratorInterface::HYDRATE_ENTITY); 27 | } 28 | 29 | /** 30 | * Get relation query 31 | * 32 | * @return \Blast\Orm\Query 33 | */ 34 | public function getQuery() 35 | { 36 | return parent::getQuery()->setMaxResults(1); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/Entity/TransformerInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 17.03.2016 10 | * Time: 08:02 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Entity; 15 | 16 | /** 17 | * Transform configuration into entity and entity definition. 18 | * 19 | * @package Blast\Orm\Entity 20 | */ 21 | interface TransformerInterface 22 | { 23 | 24 | /** 25 | * Transform configuration into entity and entity definition. Configuration could be a 26 | * string (class name or table name), array (convert to a definition), 27 | * a Definition instance or an Entity instance. 28 | * 29 | * @param $configuration 30 | * @return mixed 31 | */ 32 | public function transform($configuration); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Query/Events/AbstractQueryEvent.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 25.02.2016 10 | * Time: 13:21 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Query\Events; 15 | 16 | 17 | use League\Event\AbstractEvent; 18 | 19 | abstract class AbstractQueryEvent extends AbstractEvent 20 | { 21 | 22 | private $canceled = false; 23 | 24 | /** 25 | * Check if execution of query is canceled 26 | * @return boolean 27 | */ 28 | public function isCanceled() 29 | { 30 | return $this->canceled; 31 | } 32 | 33 | /** 34 | * @param boolean $canceled 35 | */ 36 | public function setCanceled($canceled) 37 | { 38 | $this->canceled = $canceled; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/Entity/EntityAwareTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 23.02.2016 10 | * Time: 10:20 11 | * 12 | */ 13 | namespace Blast\Orm\Entity; 14 | 15 | trait EntityAwareTrait 16 | { 17 | /** 18 | * @var array|\stdClass|\ArrayObject|object 19 | */ 20 | private $entity; 21 | 22 | /** 23 | * @return array|\stdClass|\ArrayObject|object 24 | */ 25 | public function getEntity() 26 | { 27 | return $this->entity; 28 | } 29 | 30 | /** 31 | * @param array|\ArrayObject|\stdClass|object|string $entity 32 | * @return $this 33 | */ 34 | public function setEntity($entity) 35 | { 36 | $this->entity = $entity; 37 | return $this; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/RepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 29.02.2016 10 | * Time: 10:20 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | interface RepositoryInterface 18 | { 19 | /** 20 | * Find entity by primary key 21 | * 22 | * @param mixed $primaryKey 23 | * @return \ArrayObject|\stdClass|object|array 24 | */ 25 | public function find($primaryKey); 26 | 27 | /** 28 | * Get a collection of all entities 29 | * 30 | * @return \SplStack|array 31 | */ 32 | public function all(); 33 | 34 | /** 35 | * Save new or existing entity 36 | * 37 | * @param object|array $data 38 | * @return int 39 | */ 40 | public function save($data); 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Marco Bunge 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 | -------------------------------------------------------------------------------- /src/Hydrator/HydratorInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 01.03.2016 10 | * Time: 15:46 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Hydrator; 15 | 16 | 17 | use Zend\Hydrator\ExtractionInterface; 18 | 19 | interface HydratorInterface 20 | { 21 | /** 22 | * 23 | */ 24 | const HYDRATE_COLLECTION = 'collection'; 25 | /** 26 | * 27 | */ 28 | const HYDRATE_ENTITY = 'entity'; 29 | 30 | /** 31 | * 32 | */ 33 | const HYDRATE_RAW = 'raw'; 34 | 35 | /** 36 | * 37 | */ 38 | const HYDRATE_AUTO = 'auto'; 39 | 40 | /** 41 | * Hydrate values to given object 42 | * 43 | * @param array $data 44 | * @param string $option 45 | * @return mixed 46 | */ 47 | public function hydrate($data = [], $option = self::HYDRATE_AUTO); 48 | 49 | /** 50 | * Extract values from given object 51 | * 52 | * @return array 53 | */ 54 | public function extract(); 55 | } 56 | -------------------------------------------------------------------------------- /src/ConnectionAwareTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 15.03.2016 10 | * Time: 20:32 11 | */ 12 | 13 | namespace Blast\Orm; 14 | 15 | 16 | trait ConnectionAwareTrait 17 | { 18 | 19 | /** 20 | * @var \Doctrine\DBAL\Connection|\Blast\Orm\Connection 21 | */ 22 | private $connection = null; 23 | 24 | /** 25 | * Get current connection 26 | * 27 | * @return \Doctrine\DBAL\Driver\Connection|\Doctrine\DBAL\Connection|\Blast\Orm\Connection 28 | */ 29 | public function getConnection() 30 | { 31 | if(null === $this->connection){ 32 | $this->connection = ConnectionManager::getInstance()->get(); 33 | } 34 | return $this->connection; 35 | } 36 | 37 | /** 38 | * @param \Doctrine\DBAL\Driver\Connection|\Blast\Orm\Connection|null $connection 39 | * 40 | * @return $this 41 | */ 42 | public function setConnection($connection) 43 | { 44 | $this->connection = $connection; 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ConnectionManagerInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 25.01.2016 10 | * Time: 16:08 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Query\Events; 15 | 16 | 17 | use Blast\Orm\Query; 18 | 19 | class QueryBuilderEvent extends AbstractQueryEvent 20 | { 21 | /** 22 | * @var string 23 | */ 24 | private $name; 25 | 26 | /** 27 | * @var Query 28 | */ 29 | private $builder; 30 | 31 | /** 32 | * ResultEvent constructor. 33 | * @param string $name 34 | * @param Query $builder 35 | */ 36 | public function __construct($name, $builder) 37 | { 38 | $this->name = $name; 39 | $this->builder = $builder; 40 | } 41 | 42 | /** 43 | * Return event name 44 | * @return string 45 | */ 46 | public function getName() 47 | { 48 | return $this->name; 49 | } 50 | 51 | /** 52 | * @return Query 53 | */ 54 | public function getBuilder() 55 | { 56 | return $this->builder; 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blast/orm", 3 | "description": "Framework agnostic data access and persistence based on Doctrine 2 DBAL", 4 | "keywords": [ 5 | "sql", 6 | "database", 7 | "mapper", 8 | "repository", 9 | "entity", 10 | "orm", 11 | "query" 12 | ], 13 | "homepage": "https://github.com/phpthinktank/blast-orm", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Marco Bunge", 18 | "email": "mjls@web.de", 19 | "homepage": "http://www.marco-bunge.com", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": "^5.5 || ^7.0", 25 | "doctrine/dbal": "2.5.*", 26 | "doctrine/inflector": "1.*", 27 | "doctrine/cache": "1.*", 28 | "league/event": "2.*", 29 | "minime/annotations": "^2.3", 30 | "zendframework/zend-hydrator": "2.*" 31 | }, 32 | "require-dev": { 33 | "phpunit/phpunit": "4.*" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Blast\\Orm\\": "src" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Blast\\Tests\\Orm\\": "tests" 43 | } 44 | }, 45 | "scripts": { 46 | "test": [ 47 | "cd vendor/phpunit/phpunit", 48 | "phpunit --configuration phpunit.xml" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Query/Events/QueryResultEvent.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 25.01.2016 10 | * Time: 16:08 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Query\Events; 15 | 16 | 17 | class QueryResultEvent extends AbstractQueryEvent 18 | { 19 | /** 20 | * @var string 21 | */ 22 | private $name; 23 | 24 | /** 25 | * @var boolean|array 26 | */ 27 | private $result; 28 | 29 | /** 30 | * ResultEvent constructor. 31 | * @param string $name 32 | * @param boolean|array $result 33 | */ 34 | public function __construct($name, $result) 35 | { 36 | $this->name = $name; 37 | $this->result = $result; 38 | } 39 | 40 | /** 41 | * Return event name 42 | * @return string 43 | */ 44 | public function getName() 45 | { 46 | return $this->name; 47 | } 48 | 49 | /** 50 | * @return array|bool 51 | */ 52 | public function getResult() 53 | { 54 | return $this->result; 55 | } 56 | 57 | /** 58 | * @param array|bool $result 59 | */ 60 | public function setResult($result) 61 | { 62 | $this->result = $result; 63 | } 64 | 65 | 66 | } 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/phpthinktank/blast-orm). 6 | 7 | ## Pull Requests 8 | 9 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 10 | 11 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 12 | 13 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 14 | 15 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 16 | 17 | - **Create feature branches** - Don't ask us to pull from your master branch. 18 | 19 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 20 | 21 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 22 | 23 | ## Install 24 | 25 | ``` bash 26 | $ composer install --dev 27 | ``` 28 | 29 | ## Running Tests 30 | 31 | ``` bash 32 | $ composer test 33 | ``` 34 | 35 | **Happy coding**! 36 | -------------------------------------------------------------------------------- /src/Relations/RelationTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 29.02.2016 10 | * Time: 12:06 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Relations; 15 | 16 | 17 | use Blast\Orm\Entity\ProviderFactoryInterface; 18 | use Blast\Orm\Query; 19 | 20 | trait RelationTrait 21 | { 22 | 23 | /** 24 | * @var Query 25 | */ 26 | protected $query = null; 27 | 28 | /** 29 | * @var string 30 | */ 31 | protected $name = null; 32 | 33 | /** 34 | * Query for accessing related data 35 | * 36 | * @return Query 37 | */ 38 | abstract public function getQuery(); 39 | 40 | /** 41 | * Get local entity 42 | * 43 | * @return mixed 44 | */ 45 | abstract protected function getEntity(); 46 | 47 | /** 48 | * Get foreign entity 49 | * 50 | * @return mixed 51 | */ 52 | abstract protected function getForeignEntity(); 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getName() 58 | { 59 | if($this instanceof ProviderFactoryInterface){ 60 | $this->name = $this->createProvider($this->getForeignEntity())->getDefinition()->getTableName(); 61 | } 62 | return $this->name; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/EventEmitterFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 17.03.2016 10 | * Time: 12:09 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | interface EventEmitterFactoryInterface 18 | { 19 | /** 20 | * Create event emitter and set optional events as array or \League\Event\ListenerProviderInterface. 21 | * 22 | * Event array could have a name-handler-pair, a listener provider as instance of 23 | * `\League\Event\ListenerProviderInterface` or a argument array with name, handler and priority 24 | * 25 | * ``` 26 | * $events = [ 27 | * // name-handler-pair 28 | * 'eventName' => function(){}, 29 | * 30 | * // listener provider as instance of \League\Event\ListenerProviderInterface 31 | * new \Acme\MyListenerProvider 32 | * 33 | * // argument array name, handler, prio 34 | * ['name', function(){}, 10] 35 | * 36 | * // alternating argument array name => [handler, prio] 37 | * 'eventName' => [function(){}, 10] 38 | * 39 | * ]; 40 | * ``` 41 | * 42 | * @param array $events 43 | * 44 | * @return \League\Event\Emitter 45 | */ 46 | public function createEventEmitter($events = []); 47 | } 48 | -------------------------------------------------------------------------------- /src/GatewayInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 13.04.2016 10 | * Time: 17:08 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | /** 17 | * The gateway is accessing a single table or view and interacts with database 18 | * 19 | * @package Blast\Orm 20 | */ 21 | interface GatewayInterface 22 | { 23 | 24 | /** 25 | * Create a new gateway for a single table 26 | * 27 | * @param $table 28 | */ 29 | public function __construct($table); 30 | 31 | /** 32 | * Prepare insert statement 33 | * 34 | * @param $data 35 | * @param \Doctrine\DBAL\Schema\Column[] $fields 36 | * 37 | * @return $this 38 | */ 39 | public function insert($data, $fields = []); 40 | 41 | /** 42 | * Prepare update statement 43 | * 44 | * @param $primaryKeyName 45 | * @param $data 46 | * @param \Doctrine\DBAL\Schema\Column[] $fields 47 | * 48 | * @return mixed 49 | */ 50 | public function update($primaryKeyName, $data, $fields = []); 51 | 52 | /** 53 | * Prepare delete statement 54 | * 55 | * @param $primaryKeyName 56 | * @param $primaryKey 57 | * 58 | * @return mixed 59 | */ 60 | public function delete($primaryKeyName, $primaryKey); 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/Entity/DefinitionInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 17.03.2016 10 | * Time: 08:08 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Entity; 15 | 16 | use Blast\Orm\MapperAwareInterface; 17 | use Blast\Orm\Relations\RelationsAwareInterface; 18 | use League\Event\EmitterAwareInterface; 19 | 20 | /** 21 | * All entity definition relating to schema definition 22 | * 23 | * @package Blast\Orm\Entity 24 | */ 25 | interface DefinitionInterface extends EmitterAwareInterface, MapperAwareInterface, RelationsAwareInterface 26 | { 27 | /** 28 | * @return \ArrayObject|object 29 | */ 30 | public function getEntity(); 31 | 32 | /** 33 | * @return \SplStack|object 34 | */ 35 | public function getEntityCollection(); 36 | 37 | /** 38 | * @return \Doctrine\DBAL\Schema\Column[] 39 | */ 40 | public function getFields(); 41 | 42 | /** 43 | * @return \Doctrine\DBAL\Schema\Index[] 44 | */ 45 | public function getIndexes(); 46 | 47 | /** 48 | * Name of primary key 49 | * 50 | * @return string 51 | */ 52 | public function getPrimaryKeyName(); 53 | 54 | /** 55 | * Table name 56 | * 57 | * @param bool $withPrefix 58 | * @return string 59 | */ 60 | public function getTableName($withPrefix = true); 61 | } 62 | -------------------------------------------------------------------------------- /src/MapperInterface.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 19.03.2016 10 | * Time: 12:44 11 | */ 12 | 13 | namespace Blast\Orm; 14 | 15 | use Blast\Orm\Hydrator\HydratorInterface; 16 | 17 | abstract class AbstractRepository implements MapperFactoryInterface, RepositoryInterface 18 | { 19 | 20 | use MapperFactoryTrait; 21 | 22 | /** 23 | * Get entity for repository 24 | * 25 | * @return object 26 | */ 27 | abstract public function getEntity(); 28 | 29 | /** 30 | * Get a collection of all entities 31 | * 32 | * @return \SplStack|array 33 | */ 34 | public function all() 35 | { 36 | return $this->createMapper($this->getEntity())->select()->execute(HydratorInterface::HYDRATE_COLLECTION); 37 | } 38 | 39 | /** 40 | * Find entity by primary key 41 | * 42 | * @param mixed $primaryKey 43 | * @return \ArrayObject|\stdClass|object 44 | */ 45 | public function find($primaryKey) 46 | { 47 | return $this->createMapper($this->getEntity())->find($primaryKey)->execute(HydratorInterface::HYDRATE_ENTITY); 48 | } 49 | 50 | /** 51 | * Save new or existing entity data 52 | * 53 | * @param object|array $data 54 | * @return int|bool 55 | */ 56 | public function save($data) 57 | { 58 | return $this->createMapper($data)->save($data)->execute(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Support.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 27.07.2016 10 | * Time: 11:07 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | use Doctrine\Common\Cache\Cache; 18 | 19 | class Support 20 | { 21 | 22 | /** 23 | * Get a list of PHP defined classes 24 | * 25 | * @param $nameOrObject 26 | * @return bool 27 | */ 28 | public static function isPHPInternalClass($nameOrObject){ 29 | return in_array(ltrim((string)static::getClass($nameOrObject), '\\'), spl_classes()); 30 | } 31 | 32 | /** 33 | * Get class of given FQCN or Object 34 | * 35 | * @param $nameOrObject 36 | * @return bool|string 37 | */ 38 | public static function getClass($nameOrObject){ 39 | if(is_string($nameOrObject) && class_exists($nameOrObject)){ 40 | return $nameOrObject; 41 | } 42 | 43 | if(is_object($nameOrObject)){ 44 | return get_class($nameOrObject); 45 | } 46 | 47 | return false; 48 | } 49 | 50 | /** 51 | * @param $entity 52 | * @param Cache $cache 53 | * @return \ReflectionClass 54 | */ 55 | public static function getCachedReflectionClass($entity, Cache $cache) 56 | { 57 | $entityFQCN = static::getClass($entity); 58 | $isPHPInternalClass = static::isPHPInternalClass($entityFQCN); 59 | 60 | // load from cache if entity is a valid and no internal php class 61 | if(false !== $entityFQCN && false === $isPHPInternalClass){ 62 | if($cache->contains($entityFQCN)){ 63 | return $cache->fetch($entityFQCN); 64 | } 65 | } 66 | 67 | $reflection = new \ReflectionClass($entity); 68 | 69 | // avoid caching if entity is an internal php class 70 | if(true === $isPHPInternalClass){ 71 | return $reflection; 72 | } 73 | 74 | $cache->save($entityFQCN, $reflection); 75 | 76 | return $cache->fetch($entityFQCN); 77 | } 78 | 79 | /** 80 | * @param $entity 81 | * @return bool|string 82 | */ 83 | public static function getEntityName($entity){ 84 | $fqcn = static::getClass($entity); 85 | return false === static::isPHPInternalClass($fqcn) && false !== $fqcn ? 86 | $fqcn : false; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/EventEmitterFactoryTrait.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 17.03.2016 10 | * Time: 11:27 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | use League\Event\Emitter; 18 | use League\Event\EmitterInterface; 19 | use League\Event\ListenerAcceptorInterface; 20 | use League\Event\ListenerInterface; 21 | use League\Event\ListenerProviderInterface; 22 | 23 | trait EventEmitterFactoryTrait 24 | { 25 | 26 | /** 27 | * Create event emitter and set optional events as array or \League\Event\ListenerProviderInterface. 28 | * Set as second argument a emitter instance, otherwise the factory creates a new one. 29 | * 30 | * Event array could have a name-handler-pair, a listener provider as instance of 31 | * `\League\Event\ListenerProviderInterface` or a argument array with name, handler and priority 32 | * 33 | * Configure event array as follows: 34 | * 35 | * ``` 36 | * $events = [ 37 | * // name-handler-pair 38 | * 'eventName' => function(){}, 39 | * 40 | * // listener provider as instance of \League\Event\ListenerProviderInterface 41 | * new \Acme\MyListenerProvider 42 | * 43 | * // argument array name, handler, prio 44 | * ['name', function(){}, 10] 45 | * 46 | * // alternating argument array name => [handler, prio] 47 | * 'eventName' => [function(){}, 10] 48 | * 49 | * ]; 50 | * ``` 51 | * 52 | * @param \League\Event\ListenerProviderInterface|array $events 53 | * @param \League\Event\EmitterInterface $emitter 54 | * 55 | * @return Emitter 56 | */ 57 | public function createEventEmitter($events = [], EmitterInterface $emitter = null) 58 | { 59 | if(null === $emitter){ 60 | $emitter = new Emitter(); 61 | } 62 | 63 | if (!is_array($events)) { 64 | $events = [$events]; 65 | } 66 | 67 | if (!empty($events)) { 68 | foreach ($events as $name => $handler) { 69 | 70 | if ($handler instanceof ListenerProviderInterface) { 71 | $emitter->useListenerProvider($handler); 72 | continue; 73 | } 74 | 75 | $args = is_array($handler) ? 76 | array_merge([$name], $handler) : 77 | [$name, $handler, EmitterInterface::P_NORMAL]; 78 | 79 | call_user_func_array([$emitter, 'addListener'], $args); 80 | } 81 | } 82 | 83 | return $emitter; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/Relations/BelongsTo.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 29.02.2016 10 | * Time: 12:04 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Relations; 15 | 16 | 17 | use Blast\Orm\ConnectionAwareInterface; 18 | use Blast\Orm\ConnectionAwareTrait; 19 | use Blast\Orm\Entity\EntityAwareTrait; 20 | use Blast\Orm\Entity\ProviderFactoryInterface; 21 | use Blast\Orm\Entity\ProviderFactoryTrait; 22 | use Blast\Orm\Hydrator\HydratorInterface; 23 | use Blast\Orm\Query; 24 | use Doctrine\Common\Inflector\Inflector; 25 | 26 | class BelongsTo implements ConnectionAwareInterface, RelationInterface, ProviderFactoryInterface 27 | { 28 | use ConnectionAwareTrait; 29 | use EntityAwareTrait; 30 | use ProviderFactoryTrait; 31 | use RelationTrait; 32 | 33 | /** 34 | * @var 35 | */ 36 | private $foreignEntity; 37 | /** 38 | * @var null 39 | */ 40 | private $localKey; 41 | 42 | /** 43 | * Local entity belongs to foreign entity by local key 44 | * 45 | * @param $entity 46 | * @param $foreignEntity 47 | * @param null $localKey 48 | */ 49 | public function __construct($entity, $foreignEntity, $localKey = null) 50 | { 51 | $this->entity = $entity; 52 | $this->foreignEntity = $foreignEntity; 53 | $this->localKey = $localKey; 54 | } 55 | 56 | /** 57 | * @return array|\ArrayObject|object|bool 58 | */ 59 | public function execute() 60 | { 61 | return $this->getQuery()->execute(HydratorInterface::HYDRATE_AUTO); 62 | } 63 | 64 | /** 65 | * Get relation query 66 | * 67 | * @return \Blast\Orm\Query 68 | */ 69 | public function getQuery() 70 | { 71 | if(null !== $this->query){ 72 | return $this->query; 73 | } 74 | $provider = $this->createProvider($this->getEntity()); 75 | $foreignProvider = $this->createProvider($this->getForeignEntity()); 76 | $localKey = $this->getLocalKey(); 77 | 78 | if ($localKey === null) { 79 | $localKey = Inflector::singularize($foreignProvider->getDefinition()->getTableName()) . '_' . $foreignProvider->getDefinition()->getPrimaryKeyName(); 80 | } 81 | 82 | $data = $provider->extract(); 83 | 84 | //find primary key 85 | $primaryKey = $data[$localKey]; 86 | 87 | $mapper = $foreignProvider->getDefinition()->getMapper()->setConnection($this->getConnection()); 88 | 89 | //if no primary key is available, return a select 90 | $this->query = $primaryKey === null ? $mapper->select() : $mapper->find($primaryKey); 91 | 92 | return $this->query; 93 | } 94 | 95 | /** 96 | * @return mixed 97 | */ 98 | public function getForeignEntity() 99 | { 100 | return $this->foreignEntity; 101 | } 102 | 103 | /** 104 | * @return null 105 | */ 106 | public function getLocalKey() 107 | { 108 | return $this->localKey; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/Gateway.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 13.04.2016 10 | * Time: 17:23 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | use Blast\Orm\Relations\RelationInterface; 18 | use Doctrine\DBAL\Types\Type; 19 | 20 | class Gateway implements GatewayInterface, ConnectionAwareInterface 21 | { 22 | 23 | use ConnectionAwareTrait; 24 | 25 | /** 26 | * @var string 27 | */ 28 | private $table; 29 | 30 | /** 31 | * Create a new gateway for a single table 32 | * 33 | * @param $table 34 | */ 35 | public function __construct($table) 36 | { 37 | $this->table = $table; 38 | } 39 | 40 | /** 41 | * Prepare insert statement 42 | * 43 | * @param $data 44 | * 45 | * @param \Doctrine\DBAL\Schema\Column[] $fields 46 | * @return Query|bool 47 | */ 48 | public function insert($data, $fields = []) 49 | { 50 | //cancel if $data has no entries 51 | if (count($data) < 1) { 52 | return false; 53 | } 54 | 55 | //prepare statement 56 | $query = $this->getConnection()->createQuery(); 57 | $query->insert($this->table); 58 | $this->addDataToQuery($data, $fields, $query); 59 | 60 | return $query; 61 | } 62 | 63 | /** 64 | * Prepare update statement 65 | * 66 | * @param $primaryKeyName 67 | * @param $data 68 | * @param \Doctrine\DBAL\Schema\Column[] $fields 69 | * 70 | * @return mixed 71 | */ 72 | public function update($primaryKeyName, $data, $fields = []) 73 | { 74 | //prepare statement 75 | $query = $this->getConnection()->createQuery(); 76 | $query->update($this->table); 77 | 78 | $this->addDataToQuery($data, $fields, $query); 79 | 80 | return $query->where($query->expr()->eq($primaryKeyName, $data[$primaryKeyName])); 81 | } 82 | 83 | /** 84 | * Prepare delete statement 85 | * 86 | * @param $primaryKeyName 87 | * @param $primaryKey 88 | * 89 | * @return mixed 90 | */ 91 | public function delete($primaryKeyName, $primaryKey) 92 | { 93 | $query = $this->getConnection()->createQuery(); 94 | $query 95 | ->delete($this->table) 96 | ->where($query->expr()->eq($primaryKeyName, $query->createPositionalParameter($primaryKey))); 97 | 98 | return $query; 99 | 100 | } 101 | 102 | /** 103 | * 104 | * @todo determine exclusion from gateway and integration into query similar to php value convert 105 | * 106 | * @param $data 107 | * @param \Doctrine\DBAL\Schema\Column[] $fields 108 | * @param Query $query 109 | */ 110 | protected function addDataToQuery($data, $fields, Query $query) 111 | { 112 | foreach ($data as $key => $value) { 113 | $query->addColumnValue($key, $query->createPositionalParameter( 114 | $value, array_key_exists($key, $fields) ? 115 | $fields[$key]->getType()->getName() : 116 | Type::STRING)); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Relations/HasMany.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 29.02.2016 10 | * Time: 12:14 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Relations; 15 | 16 | use Blast\Orm\ConnectionAwareInterface; 17 | use Blast\Orm\ConnectionAwareTrait; 18 | use Blast\Orm\Entity\EntityAwareTrait; 19 | use Blast\Orm\Entity\Provider; 20 | use Blast\Orm\Entity\ProviderFactoryInterface; 21 | use Blast\Orm\Entity\ProviderFactoryTrait; 22 | use Blast\Orm\Hydrator\HydratorInterface; 23 | use Blast\Orm\Query; 24 | use Doctrine\Common\Inflector\Inflector; 25 | 26 | class HasMany implements ConnectionAwareInterface, ProviderFactoryInterface, RelationInterface 27 | { 28 | 29 | use ConnectionAwareTrait; 30 | use EntityAwareTrait; 31 | use ProviderFactoryTrait; 32 | use RelationTrait; 33 | 34 | /** 35 | * @var 36 | */ 37 | private $foreignEntity; 38 | /** 39 | * @var null 40 | */ 41 | private $foreignKey; 42 | 43 | /** 44 | * Local entity relates to many entries of foreign entity by foreign key 45 | * 46 | * @param $entity 47 | * @param $foreignEntity 48 | * @param null $foreignKey 49 | */ 50 | public function __construct($entity, $foreignEntity, $foreignKey = null) 51 | { 52 | $this->entity = $entity; 53 | $this->foreignEntity = $foreignEntity; 54 | $this->foreignKey = $foreignKey; 55 | } 56 | 57 | /** 58 | * @return \\ArrayObject 59 | */ 60 | public function execute() 61 | { 62 | return $this->getQuery()->execute(HydratorInterface::HYDRATE_COLLECTION); 63 | } 64 | 65 | /** 66 | * Get relation query 67 | * 68 | * @return \Blast\Orm\Query 69 | */ 70 | public function getQuery() 71 | { 72 | if(null !== $this->query){ 73 | return $this->query; 74 | } 75 | $provider = $this->createProvider($this->getEntity()); 76 | $foreignProvider = $this->createProvider($this->getForeignEntity()); 77 | $foreignKey = $this->getForeignKey(); 78 | 79 | $data = $provider->extract(); 80 | 81 | //find primary key 82 | if ($foreignKey === null) { 83 | $foreignKey = Inflector::singularize($provider->getDefinition()->getTableName()) . '_' . $provider->getDefinition()->getPrimaryKeyName(); 84 | } 85 | 86 | $mapper = $foreignProvider->getDefinition()->getMapper()->setConnection($this->getConnection()); 87 | 88 | $foreignKeyValue = isset($data[$provider->getDefinition()->getPrimaryKeyName()]) ? $data[$provider->getDefinition()->getPrimaryKeyName()] : false; 89 | 90 | //if no primary key is available, return a select 91 | $query = $mapper->select(); 92 | if ($foreignKeyValue !== false) { 93 | $query->where((new Query($this->getConnection()))->expr()->eq($foreignKey, $foreignKeyValue)); 94 | } 95 | $this->query = $query; 96 | 97 | return $this->query; 98 | } 99 | 100 | /** 101 | * @return mixed 102 | */ 103 | public function getForeignEntity() 104 | { 105 | return $this->foreignEntity; 106 | } 107 | 108 | /** 109 | * @return null 110 | */ 111 | public function getForeignKey() 112 | { 113 | return $this->foreignKey; 114 | } 115 | 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/Connection.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 13.04.2016 10 | * Time: 08:20 11 | * 12 | */ 13 | 14 | namespace Blast\Orm; 15 | 16 | 17 | use Doctrine\Common\Cache\ArrayCache; 18 | use Doctrine\Common\Cache\Cache; 19 | use Doctrine\Common\EventManager; 20 | use Doctrine\DBAL\Configuration; 21 | use Doctrine\DBAL\Connection as DbalConnection; 22 | use Doctrine\DBAL\Driver; 23 | use Doctrine\DBAL\Query\QueryBuilder; 24 | 25 | /** 26 | * The connection wrapper provides connection-aware mapper and query based on current connection 27 | * 28 | * 29 | * @package Blast\Orm 30 | */ 31 | class Connection extends DbalConnection implements MapperFactoryInterface, QueryFactoryInterface 32 | { 33 | 34 | /** 35 | * Table name prefix for connections 36 | * @var null|string 37 | */ 38 | private $prefix = null; 39 | 40 | /** 41 | * 42 | * @var null|\Doctrine\Common\Cache\Cache 43 | */ 44 | private $internalCache = null; 45 | 46 | /** 47 | * 48 | * @var null|\Doctrine\Common\Cache\Cache 49 | */ 50 | private $metaDataCache = null; 51 | 52 | 53 | /** 54 | * 55 | * @var null|\Doctrine\Common\Cache\Cache 56 | */ 57 | private $reflectionCache = null; 58 | 59 | use MapperFactoryTrait { 60 | createMapper as protected internalCreateMapper; 61 | } 62 | 63 | public function __construct(array $params, Driver $driver, $config, $eventManager) 64 | { 65 | parent::__construct($params, $driver, $config, $eventManager); 66 | } 67 | 68 | 69 | /** 70 | * Factory method for create a new Mapper for given entity. 71 | * 72 | * * ```php 73 | * 74 | * //create mapper from connection 75 | * $connection->createMapper(Post::class); 76 | * 77 | * ``` 78 | * 79 | * @param $entity 80 | * 81 | * @return Mapper 82 | */ 83 | public function createMapper($entity){ 84 | return $this->internalCreateMapper($entity, $this); 85 | } 86 | 87 | /** 88 | * Factory method for create a new query for given entity with optional custom query builder. 89 | * 90 | * @param $entity 91 | * 92 | * @param \Doctrine\DBAL\Query\QueryBuilder $builder 93 | * 94 | * @return \Blast\Orm\Query 95 | */ 96 | public function createQuery($entity = null, QueryBuilder $builder = null) 97 | { 98 | $query = new Query($this, $entity); 99 | $query->setBuilder(null === $builder ? parent::createQueryBuilder() : $builder); 100 | return $query; 101 | } 102 | 103 | /** 104 | * @return null|string 105 | */ 106 | public function getPrefix() 107 | { 108 | return $this->prefix; 109 | } 110 | 111 | /** 112 | * @param null|string $prefix 113 | * @return $this 114 | */ 115 | public function setPrefix($prefix) 116 | { 117 | $this->prefix = $prefix; 118 | return $this; 119 | } 120 | 121 | private function getInternalCache() 122 | { 123 | if (null === $this->internalCache) { 124 | $this->internalCache = new ArrayCache(); 125 | } 126 | 127 | return $this->internalCache; 128 | } 129 | 130 | /** 131 | * @return Cache|null 132 | */ 133 | public function getMetaDataCache() 134 | { 135 | if(null === $this->metaDataCache){ 136 | $cache = $this->getInternalCache(); 137 | $this->metaDataCache = clone $cache; 138 | } 139 | return $this->metaDataCache; 140 | } 141 | 142 | /** 143 | * @return Cache|null 144 | */ 145 | public function getReflectionCache() 146 | { 147 | if(null === $this->reflectionCache){ 148 | $cache = $this->getInternalCache(); 149 | $this->reflectionCache = clone $cache; 150 | } 151 | return $this->reflectionCache; 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/Entity/Provider.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 01.03.2016 10 | * Time: 09:33 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Entity; 15 | 16 | 17 | use Blast\Orm\CacheAwareTrait; 18 | use Blast\Orm\Hydrator\EntityHydrator; 19 | use Blast\Orm\Hydrator\HydratorInterface; 20 | use Blast\Orm\Support; 21 | use Doctrine\Common\Inflector\Inflector; 22 | 23 | class Provider implements ProviderInterface 24 | { 25 | 26 | use CacheAwareTrait; 27 | use EntityAwareTrait; 28 | 29 | /** 30 | * Entity definition 31 | * 32 | * @var DefinitionInterface 33 | */ 34 | private $definition; 35 | 36 | /** 37 | * Provider constructor. 38 | * 39 | * @param $tableName 40 | */ 41 | public function __construct($tableName) 42 | { 43 | $transformer = $transformer = $this->transform($tableName); 44 | 45 | $this->entity = $transformer->getEntity(); 46 | $this->definition = $transformer->getDefinition(); 47 | } 48 | 49 | /** 50 | * @return DefinitionInterface 51 | */ 52 | public function getDefinition() 53 | { 54 | return $this->definition; 55 | } 56 | 57 | /** 58 | * Convert data array to entity with data 59 | * 60 | * @param array $data 61 | * @param string $option 62 | * @return object|\ArrayObject 63 | */ 64 | public function hydrate(array $data = [], $option = HydratorInterface::HYDRATE_AUTO) 65 | { 66 | return (new EntityHydrator($this))->hydrate($data, $option); 67 | } 68 | 69 | /** 70 | * Convert object properties or object getter to data array 71 | * 72 | * @param array $additionalData 73 | * @return array|\ArrayObject 74 | */ 75 | public function extract(array $additionalData = []) 76 | { 77 | return array_replace_recursive($additionalData, (new EntityHydrator($this))->extract()); 78 | } 79 | 80 | /** 81 | * Check if entity is new or not 82 | * 83 | * @return bool 84 | */ 85 | public function isNew() 86 | { 87 | $data = $this->extract(); 88 | 89 | return isset($data[$this->getDefinition()->getPrimaryKeyName()]) ? empty($data[$this->getDefinition()->getPrimaryKeyName()]) : true; 90 | } 91 | 92 | /** 93 | * Todo rewrite this horrobile piece of code... 94 | * @param $tableName 95 | * @return bool|string 96 | */ 97 | private function determineCacheId($tableName) 98 | { 99 | /** @var string|bool $compTableName */ 100 | if ($tableName instanceof DefinitionInterface) { 101 | return $tableName->getTableName(); 102 | } 103 | if (null === $tableName) { 104 | return false; 105 | } 106 | if (is_string($tableName)) { 107 | return 108 | (class_exists($tableName) && false === Support::isPHPInternalClass($tableName)) 109 | ? Inflector::pluralize(Inflector::tableize(Support::getCachedReflectionClass($tableName, $this->getReflectionCache())->getShortName())) 110 | : $tableName; 111 | } 112 | if (is_array($tableName)) { 113 | return false; 114 | } 115 | if(Support::isPHPInternalClass($tableName)){ 116 | return false; 117 | } 118 | if ($tableName instanceof EntityAwareInterface) { 119 | $compTableName = Inflector::pluralize(Inflector::tableize(Support::getCachedReflectionClass(Support::getEntityName($tableName->getEntity()), $this->getReflectionCache())->getShortName())); 120 | if (is_string($compTableName)) { 121 | return $compTableName; 122 | } 123 | } 124 | if (is_object($tableName)) { 125 | $compTableName = Inflector::pluralize(Inflector::tableize(Support::getCachedReflectionClass(Support::getEntityName($tableName), $this->getReflectionCache())->getShortName())); 126 | 127 | if (is_string($compTableName)) { 128 | return $compTableName; 129 | } 130 | } 131 | 132 | return false; 133 | 134 | } 135 | 136 | /** 137 | * @param $tableName 138 | * @return Transformer 139 | */ 140 | private function transform($tableName) 141 | { 142 | $transformer = new Transformer(); 143 | $transformer->transform($tableName); 144 | return $transformer; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/ConnectionManager.php: -------------------------------------------------------------------------------- 1 | $definition]; 80 | } 81 | 82 | if (!is_array($definition)) { 83 | throw new DBALException('Unable to determine parameter array from definition'); 84 | } 85 | 86 | if (!array_key_exists('wrapperClass', $definition)) { 87 | $definition['wrapperClass'] = ConnectionWrapper::class; 88 | } 89 | 90 | $connection = DriverManager::getConnection($definition); 91 | 92 | if (!($connection instanceof Connection)) { 93 | throw new \RuntimeException(sprintf('Connection needs to be an instance of %s', Connection::class)); 94 | } 95 | 96 | //setup special configuration for blast connections 97 | if ($connection instanceof \Blast\Orm\Connection) { 98 | if (array_key_exists('prefix', $definition)) { 99 | $connection->setPrefix($definition['prefix']); 100 | } 101 | } 102 | 103 | return $connection; 104 | } 105 | 106 | /** 107 | * Close all connections on 108 | */ 109 | public function __destruct() 110 | { 111 | $this->closeAll(); 112 | } 113 | 114 | /** 115 | * Disconnect all connections and remove all 116 | * connections. Collect garbage at least. 117 | */ 118 | public function closeAll() 119 | { 120 | $connections = $this->all(); 121 | 122 | foreach ($connections as $connection) { 123 | if ($connection->isConnected()) { 124 | $connection->close(); 125 | } 126 | } 127 | 128 | $this->connections = []; 129 | gc_collect_cycles(); 130 | 131 | } 132 | 133 | /** 134 | * Get all connections 135 | * 136 | * @return \Doctrine\DBAL\Connection[]|\Blast\Orm\Connection[] 137 | */ 138 | public function all() 139 | { 140 | return $this->connections; 141 | } 142 | 143 | /** 144 | * Add a new connection to internal cache. Create connection 145 | * with `Blast\Orm\ConnectionManager::create` 146 | * 147 | * @see http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#getting-a-connection 148 | * 149 | * @param array|\Doctrine\DBAL\Connection|string $connection 150 | * @param string $name 151 | * 152 | * @return $this 153 | * 154 | * @throws \Doctrine\DBAL\DBALException 155 | */ 156 | public function add($connection, $name = self::DEFAULT_CONNECTION) 157 | { 158 | if ($this->has($name)) { 159 | throw new DBALException(sprintf('Connection with name %s already exists!', $name)); 160 | } 161 | 162 | $this->connections[$name] = static::create($connection); 163 | 164 | //set first connection as active connection 165 | if (count($this->connections) === 1) { 166 | $this->swapActiveConnection($name); 167 | } 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * Check if connections exists 174 | * 175 | * @param $name 176 | * @return bool 177 | */ 178 | public function has($name) 179 | { 180 | return isset($this->connections[$name]); 181 | } 182 | 183 | /** 184 | * Swap current connection with another connection 185 | * by name and add previous connection to previous 186 | * connection stack. 187 | * 188 | * @param string $name 189 | * 190 | * @return $this 191 | * 192 | * @throws \Doctrine\DBAL\DBALException 193 | */ 194 | public function swapActiveConnection($name) 195 | { 196 | if (!$this->has($name)) { 197 | throw new DBALException(sprintf('Connection with name %s not found!', $name)); 198 | } 199 | 200 | if ($this->defaultConnection !== null) { 201 | $this->previousConnections[] = $this->defaultConnection; 202 | } 203 | $this->defaultConnection = $this->get($name); 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * Get connection by name. 210 | * 211 | * @param $name 212 | * 213 | * @return \Doctrine\DBAL\Connection|\Blast\Orm\Connection 214 | * 215 | * @throws \Doctrine\DBAL\DBALException 216 | */ 217 | public function get($name = null) 218 | { 219 | if ($name === null) { 220 | return $this->defaultConnection; 221 | } 222 | if ($this->has($name)) { 223 | return $this->connections[$name]; 224 | } 225 | 226 | throw new DBALException('Unknown connection ' . $name); 227 | } 228 | 229 | /** 230 | * @return array 231 | */ 232 | public function getPrevious() 233 | { 234 | return $this->previousConnections; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Relations/ManyToMany.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 29.02.2016 10 | * Time: 16:45 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Relations; 15 | 16 | 17 | use Blast\Orm\ConnectionAwareInterface; 18 | use Blast\Orm\ConnectionAwareTrait; 19 | use Blast\Orm\Entity\Definition; 20 | use Blast\Orm\Entity\EntityAwareTrait; 21 | use Blast\Orm\Entity\ProviderFactoryInterface; 22 | use Blast\Orm\Entity\ProviderFactoryTrait; 23 | use Blast\Orm\Hydrator\HydratorInterface; 24 | use Blast\Orm\Query; 25 | use Doctrine\Common\Util\Inflector; 26 | 27 | class ManyToMany implements ConnectionAwareInterface, ProviderFactoryInterface, RelationInterface 28 | { 29 | use ConnectionAwareTrait; 30 | use EntityAwareTrait; 31 | use ProviderFactoryTrait; 32 | use RelationTrait; 33 | 34 | /** 35 | * @var object|string 36 | */ 37 | private $foreignEntity; 38 | /** 39 | * @var null|string 40 | */ 41 | private $foreignKey; 42 | /** 43 | * @var null|string 44 | */ 45 | private $localKey; 46 | /** 47 | * @var null|object|string 48 | */ 49 | private $junction; 50 | /** 51 | * @var null|string 52 | */ 53 | private $junctionLocalKey; 54 | /** 55 | * @var null|string 56 | */ 57 | private $junctionForeignKey; 58 | 59 | 60 | /** 61 | * Many occurrences in local entity relate to many occurrences in foreign entity and vice versa. 62 | * The relations are linked by a junction table. 63 | * 64 | * @param string|object $entity 65 | * @param string|object $foreignEntity 66 | * @param null|string $foreignKey Default field name is {foreign primary key name} 67 | * @param null|string $localKey Default field name is {local primary key name} 68 | * @param null|string|object $junction Default table name is {local entity table name}_{foreign entity table name} 69 | * @param null|string $junctionLocalKey Default field name is {local table name}_{$localKey} 70 | * @param null|string $junctionForeignKey Default field name is {foreign table name}_{$foreignKey} 71 | */ 72 | public function __construct($entity, $foreignEntity, $foreignKey = null, $localKey = null, 73 | $junction = null, $junctionLocalKey = null, $junctionForeignKey = null) 74 | { 75 | 76 | $this->entity = $entity; 77 | $this->foreignEntity = $foreignEntity; 78 | $this->foreignKey = $foreignKey; 79 | $this->localKey = $localKey; 80 | $this->junction = $junction; 81 | $this->junctionLocalKey = $junctionLocalKey; 82 | $this->junctionForeignKey = $junctionForeignKey; 83 | } 84 | 85 | /** 86 | * @return null|string 87 | */ 88 | public function getLocalKey() 89 | { 90 | return $this->localKey; 91 | } 92 | 93 | /** 94 | * @return \SplStack 95 | */ 96 | public function execute() 97 | { 98 | return $this->getQuery()->execute(HydratorInterface::HYDRATE_COLLECTION); 99 | } 100 | 101 | /** 102 | * Get relation query 103 | * 104 | * @return \Blast\Orm\Query 105 | */ 106 | public function getQuery() 107 | { 108 | if (null !== $this->query) { 109 | return $this->query; 110 | } 111 | $provider = $this->createProvider($this->getEntity()); 112 | $foreignProvider = $this->createProvider($this->getForeignEntity()); 113 | $foreignKey = $this->getForeignKey(); 114 | $junction = $this->getJunction(); 115 | $junctionLocalKey = $this->getJunctionLocalKey(); 116 | $junctionForeignKey = $this->getJunctionForeignKey(); 117 | 118 | $data = $provider->extract(); 119 | 120 | $localKey = $provider->getDefinition()->getPrimaryKeyName(); 121 | 122 | //determine foreign key 123 | if ($foreignKey === null) { 124 | $foreignKey = $foreignProvider->getDefinition()->getPrimaryKeyName(); 125 | } 126 | 127 | //determine through 128 | if (!is_string($junction) || $junction === null) { 129 | $junction = Inflector::singularize($provider->getDefinition()->getTableName()) . '_' . Inflector::singularize($foreignProvider->getDefinition()->getTableName()); 130 | } 131 | 132 | //determine through local key 133 | if ($junctionLocalKey === null) { 134 | $junctionLocalKey = Inflector::singularize($provider->getDefinition()->getTableName()) . '_' . $localKey; 135 | } 136 | 137 | //determine through foreign key 138 | if ($junctionForeignKey === null) { 139 | $junctionForeignKey = Inflector::singularize($foreignProvider->getDefinition()->getTableName()) . '_' . $foreignKey; 140 | } 141 | 142 | $query = new Query($provider->getDefinition()->getMapper()->getConnection()); 143 | 144 | //prepare query for foreign table 145 | $foreignQuery = $foreignProvider->getDefinition()->getMapper() 146 | ->setConnection($this->getConnection()) 147 | ->select(); 148 | 149 | //get relations by through db object 150 | if (isset($data[$localKey])) { 151 | $junctionProvider = is_string($junction) ? 152 | $this->createProvider($junction) : 153 | $junction; 154 | $junctionMapper = $junctionProvider->getDefinition()->getMapper(); 155 | $junctionMapper->setConnection($this->getConnection()); 156 | if(true){ 157 | 158 | } 159 | $results = $junctionMapper 160 | ->select([$junctionForeignKey]) 161 | ->where($query->expr()->eq($junctionLocalKey, $data[$localKey])) 162 | ->execute(HydratorInterface::HYDRATE_RAW); 163 | 164 | //set conditions on foreign query 165 | foreach ($results as $result) { 166 | $foreignQuery->where($query->expr()->eq($foreignKey, $result[$junctionForeignKey])); 167 | } 168 | } 169 | 170 | $this->query = $foreignQuery; 171 | 172 | return $this->query; 173 | } 174 | 175 | /** 176 | * @return object|string 177 | */ 178 | public function getForeignEntity() 179 | { 180 | return $this->foreignEntity; 181 | } 182 | 183 | /** 184 | * @return null|string 185 | */ 186 | public function getForeignKey() 187 | { 188 | return $this->foreignKey; 189 | } 190 | 191 | /** 192 | * @return null|object|string 193 | */ 194 | public function getJunction() 195 | { 196 | return $this->junction; 197 | } 198 | 199 | /** 200 | * @return null|string 201 | */ 202 | public function getJunctionLocalKey() 203 | { 204 | return $this->junctionLocalKey; 205 | } 206 | 207 | /** 208 | * @return null|string 209 | */ 210 | public function getJunctionForeignKey() 211 | { 212 | return $this->junctionForeignKey; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /ChangeLog-1.0.md: -------------------------------------------------------------------------------- 1 | # Changes in Blast orm 1.0 2 | 3 | All notable changes of the Blast orm 1.0 release series are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 4 | 5 | ## 0.6.9 6 | 7 | ### Fixed 8 | 9 | - Relations ignored while determining field data automatically 10 | 11 | ## 0.6.8 12 | 13 | ### Fixed 14 | 15 | - Fix assigning relations. Conflicts between camelcase and underscore are resolved. 16 | 17 | ## 0.6.7 18 | 19 | ### Altered 20 | 21 | - Transforming of fields from entity to definition is respecting defined fields and automatically declaring columns of 22 | type string and with original property name of entity for undefined fields 23 | - Hydrator extraction respects field names and entity names and accurate assignment of field names to data. 24 | 25 | ### Fixed 26 | 27 | - underscorized fields are now correctly assigned their camelcased mutators and accessors in entity 28 | 29 | ## 0.6.6 30 | 31 | ### Altered 32 | 33 | - Allow relations to replace their entity via `Blast\Orm\Entity\EntityAwareInterface` 34 | 35 | ### Fixed 36 | 37 | - Fix empty entities in relations when entities relation definition is used 38 | 39 | ## 0.6.5 40 | 41 | ### Altered 42 | 43 | - avoid plurilize for table name defined via property or method in entity 44 | 45 | ## 0.6.4 46 | 47 | ### Altered 48 | 49 | - Fix typo and return type 50 | - Refactor adding data to query statement 51 | 52 | ### Added 53 | 54 | - Add table prefixes when accessing tablename from provider 55 | - Add accessor for definitions in entities 56 | - Add array caching backend for entity reflection and entity meta data 57 | 58 | ## 0.6.3 59 | 60 | ### Altered 61 | 62 | - `\Blast\Orm\Provider` additional data extraction is now working correctly 63 | - Update Readme 64 | 65 | ## 0.6.2 66 | 67 | ### Added 68 | 69 | - add `\Blast\Orm\Gateway` as accessor of a single database table or view 70 | 71 | ## 0.6.1 72 | 73 | ### Added 74 | 75 | - `\Blast\Orm\QueryFactoryInterface` act as contract for query creation 76 | - `\Blast\Orm\Connection` acts as connection wrapper for doctrine connection and is able to create connection-aware mappers and queries. createQueryBuilder is still creating doctrine SQL query builder! 77 | 78 | ### Altered 79 | 80 | - `\Blast\Orm\MapperFactoryInterface` is not forcing to add a connection 81 | 82 | ## 0.6.0 83 | 84 | ### Altered 85 | 86 | - add `\Blast\Orm\Entity\DefinitionInterface` and use definition instead of provider 87 | - Refactor hydrators use [`zendframework/zend-hydrator`](http://framework.zend.com/manual/current/en/modules/zend.stdlib.hydrator.html) for internal logic. 88 | 89 | ### Removed 90 | 91 | - remove definition data getters from provider, you need to use `$provider->getDefiniton()->get*()` 92 | 93 | ## 0.5.3 94 | 95 | ### Altered 96 | 97 | - Upgrade minimum requirements and dependecies 98 | 99 | ## 0.5.2 100 | 101 | ### Altered 102 | 103 | - Consider fields 104 | - Convert result from sql select to php value 105 | - Convert values from sql update or create to sql value 106 | 107 | ## 0.5.1 108 | 109 | ### Fixes 110 | 111 | - Fix bug: Use configured collection from definition instead of always \SplStack 112 | 113 | ### Altered 114 | 115 | - Add definition tests for mapper and query 116 | - Rename query `before` event to `build` 117 | - Rename query `after` event to `result` 118 | - Update readme 119 | 120 | ## 0.5 121 | 122 | ### Fixes 123 | 124 | - Fix bug where plain object don't get data from hydrator 125 | - Fix bug throwing exception when attaching relation without name 126 | 127 | ### Added 128 | 129 | - `Blast\Orm\Entity\Transformer` converts any configuration into definition and entity 130 | - `Blast\Orm\Entity\Definition` is holding entity definitions. 131 | 132 | ### Altered 133 | 134 | - connection manager is now accessible as singleton via `Blast\Orm\ConnectionManager::getInstance()` 135 | - Relations don't need to initialize to get relation name 136 | - Relation internals refactored 137 | - `\Blast\Orm\Hydrator\ArrayToObjectHydrator` hydrates relations as well as data. 138 | - Get pluralized table names when table name was suggested from entity shortname 139 | - `Blast\Orm\Entity\Provider` uses definition and transformer for preparing entities 140 | - update abstract repository with new provider and mapper 141 | 142 | ### Removed 143 | 144 | - `Blast\Orm\Facades` 145 | - Locator and Container no longer dependencies for blast orm 146 | 147 | ## 0.4 148 | 149 | ### Added 150 | 151 | - `Blast\Orm\Locator` deliver methods to access providers, mappers and connections and replaces `Blast\Orm\Entity\EntityAdapterCollectionFacade` and `Blast\Orm\ConnectionCollectionFacade` 152 | - `Blast\Orm\LocatorFacade` solves IoC concerns by providing swappable locator from container. 153 | - `Blast\Orm\Entity\Provider` replaces entity adaption 154 | - `Blast\Orm\Hydrator` manages hydration from object to array and vice versa 155 | 156 | ### Altered 157 | 158 | - `Blast\Orm\Data\DataAdapter` is now delivering logic to call data 159 | - `Blast\Orm\Entity\EntityAdapter` is now delivering logic to call definitions 160 | - Rename `Blast\Orm\Entity\EntityAdapterCollection` to `Blast\Orm\Entity\EntityAdapterManager` 161 | - Connection manager simplify redundant method names. Removed `Configuration` word from `get, set, getPrevious`, `getConnections` becomes `all` 162 | - Connection initiation to mapper or query 163 | - Add query event classes 164 | 165 | ### Removed 166 | 167 | - Replace `Blast\Orm\Container` with `League\Container` 168 | - `Blast\Orm\Entity\Provider` replaces entity adapter classes and definition interfaces 169 | - `Blast\Orm\Object\ObjectAdapter` 170 | - `Blast\Orm\ConnectionCollectionFacade` 171 | - `Blast\Orm\Query\Result` 172 | - `Blast\Orm\Entity\GenericEntity` 173 | - `Blast\Orm\Query\ResultInterface` 174 | - `Blast\Orm\Data` 175 | - `Blast\Orm\Hook` 176 | 177 | ## 0.3 178 | 179 | ### Added 180 | 181 | - `Blast\Orm\Mapper` for accessing and persisting data 182 | - `Blast\Orm\Relations` component to create relations between entities 183 | - `Blast\Orm\Object\ObjectAdapterCache` for reusing entity definition 184 | - `Blast\Orm\Facades` for more customizable classes against contracts 185 | 186 | ### Altered 187 | 188 | - `Blast\Orm\Repository` is mediating between entity and mapper delivered by entity, repository queries are excluded to mappers 189 | 190 | ### Removed 191 | 192 | - `Blast\Orm\Repository::create` 193 | - `Blast\Orm\Repository::delete` 194 | - `Blast\Orm\Repository::update` 195 | - `Blast\Orm\Manager` has been replaced by ConnectionFacade 196 | 197 | ## 0.2 198 | 199 | ### Added 200 | 201 | - `Blast\Orm\Data` component for convenient data access 202 | - `Blast\Orm\Repository` mediates between entities and query 203 | - `Blast\Orm\Hook` is providing a low-level Observer-Subject implementation basically for extending logic of class methods in `Blast\Orm\Data` 204 | 205 | ### Altered 206 | 207 | - Rename `Blast\Db`to `Blast\Orm\` 208 | - `Blast\Orm\Query::execute` is now able to emit before and after on execution by type name 209 | - `Blast\Orm\Entity` component is providing adapters for accessing entity classes and determine definition and data 210 | 211 | ### Removed 212 | 213 | - `Blast\Db\Entity` component 214 | - `Blast\Db\Orm` component (replaced by repository) 215 | -------------------------------------------------------------------------------- /src/Entity/Transformer.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 17.03.2016 10 | * Time: 08:05 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Entity; 15 | 16 | use Blast\Orm\CacheAwareTrait; 17 | use Blast\Orm\Support; 18 | use Doctrine\Common\Inflector\Inflector; 19 | 20 | class Transformer implements TransformerInterface, EntityAwareInterface 21 | { 22 | 23 | use EntityAwareTrait; 24 | use CacheAwareTrait; 25 | 26 | /** 27 | * @var \Blast\Orm\Entity\DefinitionInterface 28 | */ 29 | private $definition = null; 30 | 31 | /** 32 | * Transform configuration into entity and entity definition. Configuration could be a 33 | * string (class name or table name), array (convert to a definition), 34 | * a Definition instance or an Entity instance. 35 | * 36 | * @param $configuration 37 | * @return mixed 38 | */ 39 | public function transform($configuration) 40 | { 41 | if($configuration instanceof DefinitionInterface){ 42 | $this->definition = $configuration; 43 | $this->setEntity($this->getDefinition()->getEntity()); 44 | return $this; 45 | } 46 | if(null === $configuration){ 47 | $configuration = \ArrayObject::class; 48 | } 49 | if (is_string($configuration)) { 50 | $configuration = $this->transformStringToDefinitionArray($configuration); 51 | } 52 | if (is_array($configuration)) { 53 | $this->definition = $this->transformArrayToDefinition($configuration); 54 | $this->setEntity($this->getDefinition()->getEntity()); 55 | 56 | return $this; 57 | } 58 | if ($configuration instanceof EntityAwareInterface) { 59 | $this->setEntity($configuration->getEntity()); 60 | $this->definition = $this->transformEntityToDefinition($this->getEntity()); 61 | } 62 | if (is_object($configuration)) { 63 | $this->setEntity($configuration); 64 | $this->definition = $this->transformEntityToDefinition($this->getEntity()); 65 | } 66 | 67 | return $this; 68 | 69 | } 70 | 71 | /** 72 | * @return DefinitionInterface 73 | */ 74 | public function getDefinition() 75 | { 76 | return $this->definition; 77 | } 78 | 79 | private function transformStringToDefinitionArray($configuration) 80 | { 81 | $tableOrClass = $configuration; 82 | $configuration = []; 83 | 84 | if (class_exists($tableOrClass)) { 85 | $configuration['entity'] = new $tableOrClass; 86 | $configuration['tableName'] = $tableOrClass; 87 | }else{ 88 | $configuration['tableName'] = $tableOrClass; 89 | } 90 | 91 | return $configuration; 92 | } 93 | 94 | /** 95 | * @param $configuration 96 | * @return Definition 97 | */ 98 | private function transformArrayToDefinition($configuration) 99 | { 100 | $definition = (new Definition())->setConfiguration($configuration); 101 | // transform entity to definition and overwrite configuration 102 | return $this->transformEntityToDefinition($definition->getEntity(), $definition); 103 | } 104 | 105 | /** 106 | * @param $entity 107 | * @param \Blast\Orm\Entity\DefinitionInterface|null $definition 108 | * @return Definition 109 | */ 110 | private function transformEntityToDefinition($entity, $definition = null) 111 | { 112 | // find definition class in entity by property or method 113 | $definition = $this->loadDefinitionFromEntity($entity, $definition); 114 | 115 | $reflection = Support::getCachedReflectionClass($entity, $this->getReflectionCache()); 116 | $configuration = $definition->getConfiguration(); 117 | 118 | // mapper is a dependency for all other entity services 119 | // fetch mapper first 120 | if ($reflection->hasMethod('mapper')) { 121 | $mapperMethod = $reflection->getMethod('mapper'); 122 | if($mapperMethod->isStatic() && $mapperMethod->isPublic()){ 123 | $configuration['mapper'] = $mapperMethod->invokeArgs($entity, [$entity]); 124 | } 125 | } 126 | 127 | //update configuration with mapper 128 | $configuration = $definition->setConfiguration($configuration)->getConfiguration(); 129 | 130 | foreach ($configuration as $key => $value) { 131 | //ignore entity or mapper 132 | if (in_array($key, ['entity', 'mapper'])) { 133 | continue; 134 | } 135 | if ($reflection->hasMethod($key)) { 136 | $method = $reflection->getMethod($key); 137 | if ($method->isPublic() && $method->isStatic()) { 138 | $value = $method->invokeArgs($entity, [$entity, $definition->getMapper()]); 139 | } 140 | } else { 141 | $value = is_callable($value) ? 142 | call_user_func_array($value, [$entity, $definition->getMapper()]) : 143 | $value; 144 | } 145 | $configuration[$key] = $value; 146 | } 147 | 148 | $configuration['entityClassName'] = $reflection->getName(); 149 | $configuration['entityShortName'] = $reflection->getShortName(); 150 | 151 | // fetch table name from table property 152 | if($reflection->hasProperty('table')){ 153 | $reflectionProperty = $reflection->getProperty('table'); 154 | if($reflectionProperty->isStatic()){ 155 | $configuration['tableName'] = $reflectionProperty->getValue($entity); 156 | } 157 | } 158 | 159 | // fetch table name from table method 160 | if($reflection->hasMethod('table')){ 161 | $reflectionMethod = $reflection->getMethod('table'); 162 | if($reflectionMethod->isStatic()){ 163 | $configuration['tableName'] = $reflectionMethod->invoke($entity); 164 | } 165 | } 166 | 167 | $definition->setConfiguration($configuration); 168 | 169 | // set table name from reflection short name 170 | // if table name is unknown or FQCN and if 171 | // entity is no ArrayObject 172 | if ((empty($definition->getTableName()) || class_exists($definition->getTableName()) ) && !($definition->getEntity() instanceof \ArrayObject)) { 173 | $configuration['tableName'] = Inflector::pluralize(Inflector::tableize($configuration['entityShortName'])); 174 | } 175 | 176 | return $definition->setConfiguration($configuration); 177 | } 178 | 179 | /** 180 | * @param $entity 181 | * @param Definition|null $definition 182 | * @return Definition|null 183 | */ 184 | private function loadDefinitionFromEntity($entity, $definition = null) 185 | { 186 | $definitionClass = null; 187 | if (property_exists($entity, 'definition')) { 188 | $definitionClass = $entity::$definition; 189 | } 190 | if (method_exists($entity, 'definition')) { 191 | $definitionClass = $entity::definition(); 192 | } 193 | if (null !== $definitionClass) { 194 | $definition = is_object($definitionClass) ? $definitionClass : new $definitionClass; 195 | } 196 | if (null === $definition) { 197 | $definition = new Definition(); 198 | } 199 | 200 | $configuration = $definition->getConfiguration(); 201 | $configuration['entity'] = $entity; 202 | $definition->setConfiguration($configuration); 203 | 204 | return $definition; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Hydrator/EntityHydrator.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 11.03.2016 10 | * Time: 23:24 11 | */ 12 | 13 | namespace Blast\Orm\Hydrator; 14 | 15 | use Blast\Orm\Entity\ProviderInterface; 16 | use Doctrine\Common\Inflector\Inflector; 17 | use Doctrine\DBAL\Driver\Statement; 18 | use Zend\Hydrator\ArraySerializable; 19 | use Zend\Hydrator\ClassMethods; 20 | use Zend\Hydrator\ObjectProperty; 21 | 22 | class EntityHydrator implements HydratorInterface 23 | { 24 | 25 | /** 26 | * @var \Blast\Orm\Entity\Provider 27 | */ 28 | private $provider; 29 | 30 | public function __construct(ProviderInterface $provider) 31 | { 32 | $this->provider = $provider; 33 | } 34 | 35 | /** 36 | * Extract values from given object 37 | * 38 | * @return array 39 | */ 40 | public function extract() 41 | { 42 | $entity = clone $this->provider->getEntity(); 43 | 44 | // extract from array object 45 | // ignore further extraction strategy 46 | if ( method_exists($entity, 'populate') || method_exists($entity, 'exchangeArray') ) { 47 | $hydrator = $this->getArraySerizableHydrator(); 48 | return $this->underscorizeKeys($hydrator->extract($entity)); 49 | } 50 | 51 | // extract data from class getters and properties 52 | $propertyHydrator = $this->getObjectPropertyHydrator(); 53 | $classMethodHydrator = $this->getClassMethodsHydrator(); 54 | 55 | // get a full set of original names, camelize keys and underscrorize keys 56 | $extractedDataSet = array_replace( 57 | $propertyHydrator->extract($entity), 58 | $classMethodHydrator->extract($entity), 59 | $this->camelizeKeys($propertyHydrator->extract($entity)), 60 | $this->camelizeKeys($classMethodHydrator->extract($entity)), 61 | $this->underscorizeKeys($propertyHydrator->extract($entity)), 62 | $this->underscorizeKeys($classMethodHydrator->extract($entity)) 63 | ); 64 | 65 | $fields = $this->provider->getDefinition()->getFields(); 66 | $extractedData = []; 67 | 68 | foreach ($extractedDataSet as $key => $value){ 69 | if(array_key_exists($key, $fields)){ 70 | $extractedData[$key] = $value; 71 | } 72 | } 73 | 74 | return $extractedData; 75 | } 76 | 77 | /** 78 | * @param array $data 79 | * @param string $option 80 | * @return mixed 81 | */ 82 | public function hydrate($data = [], $option = self::HYDRATE_AUTO) 83 | { 84 | $option = $this->determineOption($data, $option); 85 | 86 | switch ($option) { 87 | case self::HYDRATE_RAW: 88 | return $data; 89 | //if entity set has many items, return a collection of entities 90 | case self::HYDRATE_COLLECTION : 91 | return $this->hydrateCollection($data); 92 | //if entity has one item, return the entity 93 | case self::HYDRATE_ENTITY: 94 | $data = $this->isCollectable($data) ? array_shift($data) : $data; 95 | 96 | return $this->hydrateEntity($data); 97 | } 98 | 99 | throw new \LogicException('Unknown option ' . $option); 100 | } 101 | 102 | /** 103 | * @param $data 104 | * @return mixed 105 | */ 106 | public function isCollectable($data) 107 | { 108 | if ( ! is_array($data) ) { 109 | return false; 110 | } 111 | 112 | return is_array(reset($data)); 113 | } 114 | 115 | /** 116 | * @param $data 117 | * @param $option 118 | * @return string 119 | */ 120 | protected function determineOption($data, $option) 121 | { 122 | if ( $option === self::HYDRATE_RAW || 123 | $data instanceof Statement || 124 | is_scalar($data) || 125 | is_bool($data) || 126 | null === $data 127 | ) { 128 | return self::HYDRATE_RAW; 129 | } 130 | if ( $option === self::HYDRATE_AUTO ) { 131 | $option = $this->isCollectable($data) && (count($data) === 0 || count($data) > 1) ? self::HYDRATE_COLLECTION : self::HYDRATE_ENTITY; 132 | } 133 | 134 | return $option; 135 | } 136 | 137 | /** 138 | * @param $data 139 | * @return object|\SplStack 140 | */ 141 | protected function hydrateCollection($data) 142 | { 143 | $stack = $this->provider->getDefinition()->getEntityCollection(); 144 | foreach ($data as $key => $value) { 145 | $stack->push($this->hydrateEntity($value)); 146 | } 147 | 148 | $stack->rewind(); 149 | 150 | return $stack; 151 | } 152 | 153 | /** 154 | * Hydrates data to an entity 155 | * 156 | * @param $data 157 | * @return array|\ArrayObject|object|\stdClass 158 | */ 159 | protected function hydrateEntity($data) 160 | { 161 | $entity = clone $this->provider->getEntity(); 162 | 163 | // hydrate to array object 164 | if ( method_exists($entity, 'populate') || method_exists($entity, 'exchangeArray') ) { 165 | $hydrator = $this->getArraySerizableHydrator(); 166 | $entity = $hydrator->hydrate($data, $entity); 167 | 168 | //add relations 169 | $data = $this->addRelationsToData($data, $entity); 170 | $entity = $hydrator->hydrate($data, $entity); 171 | } 172 | 173 | $data = $this->camelizeKeys($data); 174 | 175 | $classMethodHydrator = $this->getClassMethodsHydrator(); 176 | $propertyHydrator = $this->getObjectPropertyHydrator(); 177 | 178 | // hydrate entity data 179 | $entity = $classMethodHydrator->hydrate( 180 | $data, 181 | $propertyHydrator->hydrate($data, $entity) 182 | ); 183 | 184 | //add relations 185 | $data = $this->addRelationsToData($data, $entity); 186 | 187 | // hydrate entity with relation data to add hydrated entity to relation 188 | return $classMethodHydrator->hydrate( 189 | $data, 190 | $propertyHydrator->hydrate($data, $entity) 191 | ); 192 | } 193 | 194 | /** 195 | * Add relations to data 196 | * 197 | * @param $data 198 | * @param $entity 199 | * @return mixed 200 | */ 201 | protected function addRelationsToData($data, $entity) 202 | { 203 | foreach ($this->provider->getDefinition()->getRelations() as $name => $relation) { 204 | if ( is_numeric($name) ) { 205 | $name = $relation->getName(); 206 | } 207 | // disallow overwriting existing data 208 | if ( isset($data[$name]) ) { 209 | continue; 210 | } 211 | 212 | // only attached entity is allowed! 213 | $entityClass = get_class($entity); 214 | if($relation->getEntity() instanceof $entityClass){ 215 | $relation->setEntity($entity); 216 | } 217 | $data[$name] = $relation; 218 | $data[Inflector::camelize($name)] = $relation; 219 | $data[Inflector::tableize($name)] = $relation; 220 | } 221 | 222 | return $data; 223 | } 224 | 225 | /** 226 | * Convert keys to camel case 227 | * 228 | * @param $data 229 | * @return array 230 | */ 231 | protected function camelizeKeys($data) 232 | { 233 | $result = []; 234 | foreach ($data as $key => $value) { 235 | $result[Inflector::camelize($key)] = $value; 236 | } 237 | 238 | return $result; 239 | } 240 | 241 | /** 242 | * Convert keys to underscore 243 | * 244 | * @param $data 245 | * @return array 246 | */ 247 | protected function underscorizeKeys($data) 248 | { 249 | $result = []; 250 | foreach ($data as $key => $value) { 251 | $result[Inflector::tableize($key)] = $value; 252 | } 253 | 254 | return $result; 255 | } 256 | 257 | /** 258 | * @return ArraySerializable 259 | */ 260 | protected function getArraySerizableHydrator() 261 | { 262 | return new ArraySerializable(); 263 | } 264 | 265 | /** 266 | * @return ClassMethods 267 | */ 268 | protected function getClassMethodsHydrator() 269 | { 270 | return new ClassMethods(false); 271 | } 272 | 273 | /** 274 | * @return ObjectProperty 275 | */ 276 | protected function getObjectPropertyHydrator() 277 | { 278 | $hydrator = new ObjectProperty(); 279 | 280 | return $hydrator; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/Entity/Definition.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 17.03.2016 10 | * Time: 08:17 11 | * 12 | */ 13 | 14 | namespace Blast\Orm\Entity; 15 | 16 | 17 | use Blast\Orm\ConnectionManager; 18 | use Blast\Orm\EventEmitterFactoryInterface; 19 | use Blast\Orm\EventEmitterFactoryTrait; 20 | use Blast\Orm\Mapper; 21 | use Blast\Orm\MapperFactoryInterface; 22 | use Blast\Orm\MapperFactoryTrait; 23 | use Blast\Orm\MapperInterface; 24 | use Blast\Orm\Relations\RelationInterface; 25 | use Doctrine\Common\Inflector\Inflector; 26 | use Doctrine\DBAL\Schema\Column; 27 | use Doctrine\DBAL\Types\Type; 28 | use League\Event\EmitterAwareInterface; 29 | use League\Event\EmitterAwareTrait; 30 | 31 | class Definition implements DefinitionInterface, EventEmitterFactoryInterface, MapperFactoryInterface 32 | { 33 | 34 | use EmitterAwareTrait; 35 | use EventEmitterFactoryTrait; 36 | use MapperFactoryTrait; 37 | 38 | private $configuration = [ 39 | 'entity' => \ArrayObject::class, 40 | 'entityCollection' => \SplStack::class, 41 | 'events' => [], 42 | 'fields' => [], 43 | 'indexes' => [], 44 | 'primaryKeyName' => ProviderInterface::DEFAULT_PRIMARY_KEY_NAME, 45 | 'tableName' => '', 46 | 'mapper' => Mapper::class, 47 | 'relations' => [], 48 | ]; 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function getConfiguration() 54 | { 55 | return $this->configuration; 56 | } 57 | 58 | /** 59 | * Add additional configuration. Configuration will be merged into 60 | * 61 | * @param array $configuration 62 | * @return Definition 63 | */ 64 | public function setConfiguration(array $configuration) 65 | { 66 | $this->mergeConfiguration($configuration); 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Get the entity object. 73 | * 74 | * @return \ArrayObject|object 75 | */ 76 | public function getEntity() 77 | { 78 | if (!is_object($this->configuration['entity'])) { 79 | $entity = $this->configuration['entity']; 80 | $this->configuration['entity'] = new $entity; 81 | } 82 | return $this->configuration['entity']; 83 | } 84 | 85 | /** 86 | * Get the custom entity collection 87 | * 88 | * @return \SplStack|object 89 | */ 90 | public function getEntityCollection() 91 | { 92 | if (!is_object($this->configuration['entityCollection'])) { 93 | $entity = $this->configuration['entityCollection']; 94 | $this->configuration['entityCollection'] = new $entity; 95 | } 96 | return $this->configuration['entityCollection']; 97 | } 98 | 99 | /** 100 | * Load event emitter. If entity has events and 101 | * no emitter exists, create a new emitter. 102 | * 103 | * @return \ArrayObject|object 104 | */ 105 | public function getEmitter() 106 | { 107 | if (null === $this->emitter) { 108 | if (!empty($this->configuration['events'])) { 109 | $entity = $this->getEntity(); 110 | if ($entity instanceof EmitterAwareInterface) { 111 | $emitter = $this->createEventEmitter($this->configuration['events'], $entity->getEmitter()); 112 | } else { 113 | $emitter = $this->createEventEmitter($this->configuration['events']); 114 | } 115 | } else { 116 | $emitter = $this->createEventEmitter(); 117 | } 118 | 119 | $this->setEmitter($emitter); 120 | } 121 | return $this->emitter; 122 | } 123 | 124 | /** 125 | * Get fields 126 | * 127 | * @return \Doctrine\DBAL\Schema\Column[] 128 | */ 129 | public function getFields() 130 | { 131 | return $this->configuration['fields']; 132 | } 133 | 134 | /** 135 | * Get indexes 136 | * 137 | * @return \Doctrine\DBAL\Schema\Index[] 138 | */ 139 | public function getIndexes() 140 | { 141 | return $this->configuration['indexes']; 142 | } 143 | 144 | /** 145 | * Get name of primary key 146 | * 147 | * @return string 148 | */ 149 | public function getPrimaryKeyName() 150 | { 151 | return $this->configuration['primaryKeyName']; 152 | } 153 | 154 | /** 155 | * Get table name 156 | * 157 | * Add prefix if if $withPrefix is true and a prefix exists 158 | * 159 | * @param bool $withPrefix 160 | * @return string 161 | */ 162 | public function getTableName($withPrefix = true) 163 | { 164 | $prefix = ConnectionManager::getInstance()->get()->getPrefix(); 165 | return (true === $withPrefix && strlen($prefix) > 0 ? rtrim($prefix, '_') . '_' : '') . $this->configuration['tableName']; 166 | } 167 | 168 | /** 169 | * Get entity mapper 170 | * 171 | * @return MapperInterface 172 | */ 173 | public function getMapper() 174 | { 175 | if (!($this->configuration['mapper'] instanceof MapperInterface)) { 176 | $this->configuration['mapper'] = $this->createMapper($this); 177 | } 178 | return $this->configuration['mapper']; 179 | } 180 | 181 | /** 182 | * Get an array of relations 183 | * 184 | * @return RelationInterface[] 185 | */ 186 | public function getRelations() 187 | { 188 | return $this->configuration['relations']; 189 | } 190 | 191 | /** 192 | * Merge partial configuration into definition configuration. Normalize partial 193 | * configuration keys before add them to configuration. Add custom configuration 194 | * after adding known configuration. 195 | * 196 | * @param array $configuration 197 | * 198 | * @return $this 199 | */ 200 | private function mergeConfiguration(array $configuration) 201 | { 202 | $originalConfiguration = $configuration; 203 | 204 | // normalize keys to lower case 205 | foreach ($configuration as $key => $value) { 206 | $configuration[strtolower($key)] = $value; 207 | } 208 | 209 | // add definition configuration by it's real key 210 | $configKeys = array_keys($this->configuration); 211 | foreach ($configKeys as $key) { 212 | $normalizedKey = strtolower($key); 213 | if (isset($configuration[$normalizedKey])) { 214 | $this->configuration[$key] = $configuration[$normalizedKey]; 215 | unset($configuration[$normalizedKey]); 216 | } 217 | } 218 | 219 | // add custom configuration by it's original key 220 | foreach ($originalConfiguration as $key => $value) { 221 | $normalizedKey = strtolower($key); 222 | if (isset($configuration[$normalizedKey])) { 223 | $this->configuration[$key] = $originalConfiguration[$key]; 224 | } 225 | } 226 | 227 | $this->processFields(); 228 | 229 | return $this; 230 | } 231 | 232 | /** 233 | * Setup entity fields and determine undefined fields from entity with type string 234 | */ 235 | private function processFields() 236 | { 237 | /** @var Column[] $internalFields */ 238 | $internalFields = $this->configuration['fields']; 239 | $fields = []; 240 | 241 | foreach ($internalFields as $name => $field){ 242 | // skip if field is no instance of Column 243 | if(!($field instanceof Column)){ 244 | continue; 245 | } 246 | 247 | //set table name as key 248 | if($field->getName() !== $name){ 249 | $name = $field->getName(); 250 | } 251 | 252 | $fields[$name] = $field; 253 | } 254 | 255 | $reflectionClass = new \ReflectionClass($this->getEntity()); 256 | $entityProperties = $reflectionClass->getProperties(); 257 | 258 | foreach ($entityProperties as $key => $value){ 259 | // disallow static properties 260 | if($value->isStatic()){ 261 | continue; 262 | } 263 | 264 | // skip dynamic column declaration 265 | $fieldName = $value->getName(); 266 | if( 267 | array_key_exists($fieldName, $fields) 268 | || array_key_exists(Inflector::camelize($fieldName), $fields) 269 | || array_key_exists(Inflector::tableize($fieldName), $fields) 270 | ){ 271 | continue; 272 | } 273 | 274 | $fields[$fieldName] = new Column($fieldName, Type::getType(Type::STRING)); 275 | 276 | } 277 | 278 | //remove all relations 279 | $relations = $this->getRelations(); 280 | foreach($fields as $key => $value){ 281 | if(array_key_exists($key, $relations)){ 282 | unset($fields[$key]); 283 | } 284 | } 285 | 286 | $this->configuration['fields'] = $fields; 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/Mapper.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 61 | if ( $entity instanceof DefinitionInterface ) { 62 | $this->setEntity($entity->getEntity()); 63 | $this->definition = $entity; 64 | } elseif ( $entity instanceof ProviderInterface ) { 65 | $this->setEntity($entity->getEntity()); 66 | $this->provider = $entity; 67 | } else { 68 | $this->setEntity($entity); 69 | } 70 | } 71 | 72 | /** 73 | * Select query for finding entity by primary key 74 | * 75 | * @param mixed $primaryKey 76 | * @return Query 77 | */ 78 | public function find($primaryKey) 79 | { 80 | $query = $this->select(); 81 | $query->where($query->expr()->eq( 82 | $this->getDefinition()->getPrimaryKeyName(), 83 | $query->createPositionalParameter($primaryKey)) 84 | ); 85 | 86 | return $query; 87 | } 88 | 89 | /** 90 | * Select query for entity 91 | * 92 | * @param array $selects 93 | * @return Query 94 | */ 95 | public function select($selects = ['*']) 96 | { 97 | return $this->createQuery() 98 | ->select($selects) 99 | ->from($this->getDefinition()->getTableName()); 100 | } 101 | 102 | /** 103 | * Create a new Query instance 104 | * @return \Blast\Orm\Query 105 | */ 106 | public function createQuery() 107 | { 108 | return new Query($this->getConnection(), $this->getEntity()); 109 | } 110 | 111 | /** 112 | * Create a new Query instance 113 | * @return \Blast\Orm\Gateway 114 | */ 115 | public function createGateway($tableName) 116 | { 117 | $gateway = new Gateway($tableName); 118 | $gateway->setConnection($this->getConnection()); 119 | 120 | return $gateway; 121 | } 122 | 123 | /** 124 | * @return ProviderInterface 125 | */ 126 | public function getProvider() 127 | { 128 | if ( null === $this->provider ) { 129 | $this->provider = $this->createProvider($this->getEntity()); 130 | } 131 | 132 | return $this->provider; 133 | } 134 | 135 | /** 136 | * @return DefinitionInterface 137 | */ 138 | public function getDefinition() 139 | { 140 | if ( null === $this->definition ) { 141 | $this->definition = $this->createProvider($this->getEntity())->getDefinition(); 142 | } 143 | 144 | return $this->definition; 145 | } 146 | 147 | /** 148 | * Create query for new entity. 149 | * 150 | * @param array|\ArrayObject|\stdClass|object $entity 151 | * @return Query|bool 152 | */ 153 | public function create($entity) 154 | { 155 | //load entity provider 156 | $provider = $this->prepareProvider($entity); 157 | $definition = $provider->getDefinition(); 158 | 159 | //disallow differing entities 160 | return $this->checkEntity($definition->getEntity()) 161 | ->createGateway($definition->getTableName()) 162 | ->insert($provider->extract(), $definition->getFields()); 163 | } 164 | 165 | /** 166 | * Update query for existing Model or a collection of entities in storage 167 | * 168 | * @param array|\ArrayObject|\stdClass|object $entity 169 | * @return Query 170 | */ 171 | public function update($entity) 172 | { 173 | //load entity provider 174 | $provider = $this->prepareProvider($entity); 175 | $definition = $provider->getDefinition(); 176 | 177 | //disallow differing entities 178 | return $this->checkEntity($definition->getEntity()) 179 | ->createGateway($definition->getTableName()) 180 | ->update( 181 | $definition->getPrimaryKeyName(), 182 | $provider->extract(), 183 | $definition->getFields() 184 | ); 185 | } 186 | 187 | /** 188 | * Create or update an entity 189 | * 190 | * @param \ArrayObject|\SplStack|\stdClass|object $entity 191 | * @return Query 192 | */ 193 | public function save($entity) 194 | { 195 | return $this->createProvider($entity)->isNew() ? $this->create($entity) : $this->update($entity); 196 | } 197 | 198 | /** 199 | * Prepare delete query for attached entity by identifiers 200 | * 201 | * @param int|string $identifier 202 | * @return query 203 | */ 204 | public function delete($identifier) 205 | { 206 | $definition = $this->getDefinition(); 207 | 208 | //prepare statement 209 | $primaryKeyName = $definition->getPrimaryKeyName(); 210 | 211 | if ( is_object($identifier) ) { 212 | $identifierProvider = $this->createProvider($identifier); 213 | $this->checkEntity($identifierProvider->getEntity()); 214 | $data = $identifierProvider->extract(); 215 | $identifier = $data[$primaryKeyName]; 216 | } 217 | 218 | return $this 219 | ->createGateway($definition->getTableName()) 220 | ->delete($primaryKeyName, $identifier); 221 | } 222 | 223 | /** 224 | * BelongsTo is the inverse of a HasOne or a HasMany relation. 225 | * 226 | * One entity is associated with one related entity by a field which 227 | * associates with primary key in related entity. 228 | * 229 | * @param $entity 230 | * @param $foreignEntity 231 | * @param null $localKey 232 | * 233 | * @return Query 234 | */ 235 | public function belongsTo($entity, $foreignEntity, $localKey = null) 236 | { 237 | return $this->prepareRelation(new BelongsTo($entity, $foreignEntity, $localKey)); 238 | } 239 | 240 | /** 241 | * One entity is associated with one related entity by a field which 242 | * associates with primary key in current entity. 243 | * 244 | * @param $entity 245 | * @param $foreignEntity 246 | * @param null|string $foreignKey 247 | * 248 | * @return Query 249 | */ 250 | public function hasOne($entity, $foreignEntity, $foreignKey = null) 251 | { 252 | return $this->prepareRelation(new HasOne($entity, $foreignEntity, $foreignKey)); 253 | } 254 | 255 | /** 256 | * One entity is associated with many related entities 257 | * by a field which associates with primary key in current entity. 258 | * 259 | * @param $entity 260 | * @param $foreignEntity 261 | * @param null $foreignKey 262 | * 263 | * @return Query 264 | */ 265 | public function hasMany($entity, $foreignEntity, $foreignKey = null) 266 | { 267 | return $this->prepareRelation(new HasMany($entity, $foreignEntity, $foreignKey)); 268 | } 269 | 270 | /** 271 | * Many entities of type _A_ are associated with many 272 | * related entities of type _B_ by a junction table. 273 | * The junction table stores associations from entities 274 | * of type _A_ to entities of type _B_. 275 | * 276 | * @param $entity 277 | * @param $foreignEntity 278 | * @param null $foreignKey 279 | * @param null $localKey 280 | * @param null $junction 281 | * @param null $junctionLocalKey 282 | * @param null $junctionForeignKey 283 | * 284 | * @return Query 285 | */ 286 | public function manyToMany( 287 | $entity, 288 | $foreignEntity, 289 | $foreignKey = null, 290 | $localKey = null, 291 | $junction = null, 292 | $junctionLocalKey = null, 293 | $junctionForeignKey = null 294 | ) { 295 | 296 | return $this->prepareRelation( 297 | new ManyToMany($entity, $foreignEntity, $foreignKey, 298 | $localKey, $junction, $junctionLocalKey, $junctionForeignKey) 299 | ); 300 | 301 | } 302 | 303 | /** 304 | * Prepare provider by determining entity type 305 | * 306 | * @param $entity 307 | * @return Provider 308 | */ 309 | private function prepareProvider($entity) 310 | { 311 | if ( is_array($entity) ) { 312 | $provider = $this->createProvider($this->getEntity()); 313 | 314 | // reset entity in provider and 315 | // set data 316 | $provider->setEntity($provider->hydrate($entity, HydratorInterface::HYDRATE_ENTITY)); 317 | } else { 318 | $provider = $this->createProvider($entity); 319 | } 320 | 321 | return $provider; 322 | } 323 | 324 | /** 325 | * Share mapper connection with relation 326 | * 327 | * @param $relation 328 | * @return mixed 329 | */ 330 | private function prepareRelation(RelationInterface $relation) 331 | { 332 | if ( $relation instanceof ConnectionAwareInterface ) { 333 | $relation->setConnection($this->getConnection()); 334 | } 335 | 336 | return $relation; 337 | } 338 | 339 | /** 340 | * Check if external entity matches mapper entity 341 | * 342 | * @param $entity 343 | * 344 | * @return $this 345 | * 346 | * @throws \InvalidArgumentException 347 | */ 348 | private function checkEntity($entity) 349 | { 350 | if ( get_class($entity) !== get_class($this->getDefinition()->getEntity()) ) { 351 | throw new \InvalidArgumentException('Disallowed usage of differing entity!' . get_class($entity)); 352 | } 353 | 354 | return $this; 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | 5 | * 6 | * For the full copyright and license information, please view the LICENSE.txt 7 | * file that was distributed with this source code. 8 | * 9 | * Date: 08.02.2016 10 | * Time: 16:11 11 | */ 12 | 13 | namespace Blast\Orm; 14 | 15 | use Blast\Orm\Entity\EntityAwareInterface; 16 | use Blast\Orm\Entity\EntityAwareTrait; 17 | use Blast\Orm\Entity\ProviderFactoryInterface; 18 | use Blast\Orm\Entity\ProviderFactoryTrait; 19 | use Blast\Orm\Entity\ProviderInterface; 20 | use Blast\Orm\Hydrator\EntityHydrator; 21 | use Blast\Orm\Hydrator\HydratorInterface; 22 | use Blast\Orm\Query\Events\QueryBuilderEvent; 23 | use Blast\Orm\Query\Events\QueryResultEvent; 24 | use Doctrine\DBAL\Query\QueryBuilder; 25 | use Doctrine\DBAL\Types\Type; 26 | use League\Event\EmitterAwareInterface; 27 | use League\Event\EmitterAwareTrait; 28 | use stdClass; 29 | 30 | /** 31 | * Class Query 32 | * 33 | * @method \Doctrine\DBAL\Query\Expression\ExpressionBuilder expr() 34 | * @method int getType() 35 | * @method int getState() 36 | * @method string getSQL() 37 | * @method Query setParameter($key, $value, $type = null) 38 | * @method Query setParameters(array $params, array $types = []) 39 | * @method array getParameters() 40 | * @method mixed getParameter($key) 41 | * @method array getParameterTypes() 42 | * @method mixed getParameterType($key) 43 | * @method Query setFirstResult($firstResult) 44 | * @method int getFirstResult() 45 | * @method Query setMaxResults($maxResults) 46 | * @method int getMaxResults() 47 | * @method Query add($sqlPartName, $sqlPart, $append = false) 48 | * @method Query addSelect($select = null) 49 | * @method Query delete($delete = null, $alias = null) 50 | * @method Query update($update = null, $alias = null) 51 | * @method Query insert($insert = null) 52 | * @method Query from($from, $alias = null) 53 | * @method Query join($fromAlias, $join, $alias, $condition = null) 54 | * @method Query innerJoin($fromAlias, $join, $alias, $condition = null) 55 | * @method Query leftJoin($fromAlias, $join, $alias, $condition = null) 56 | * @method Query rightJoin($fromAlias, $join, $alias, $condition = null) 57 | * @method Query set($key, $value) 58 | * @method Query where($predicates) 59 | * @method Query andWhere($where) 60 | * @method Query orWhere($where) 61 | * @method Query groupBy($groupBy) 62 | * @method Query addGroupBy($groupBy) 63 | * @method Query setValue($column, $value) 64 | * @method Query values(array $values) 65 | * @method Query having($having) 66 | * @method Query andHaving($having) 67 | * @method Query orHaving($having) 68 | * @method Query orderBy($sort, $order = null) 69 | * @method Query addOrderBy($sort, $order = null) 70 | * @method mixed getQueryPart($queryPartName) 71 | * @method array getQueryParts() 72 | * @method Query resetQueryParts($queryPartNames = null) 73 | * @method Query resetQueryPart($queryPartName) 74 | * @method string __toString() 75 | * @method string createNamedParameter($value, $type = \PDO::PARAM_STR, $placeHolder = null) 76 | * @method string createPositionalParameter($value, $type = \PDO::PARAM_STR) 77 | * @method void __clone() 78 | * 79 | * @package Blast\Db\Orm 80 | */ 81 | class Query implements ConnectionAwareInterface, EmitterAwareInterface, 82 | EntityAwareInterface, ProviderFactoryInterface, QueryInterface 83 | { 84 | use ConnectionAwareTrait; 85 | use EmitterAwareTrait; 86 | use EntityAwareTrait; 87 | use ProviderFactoryTrait; 88 | 89 | /** 90 | * @var QueryBuilder 91 | */ 92 | private $builder; 93 | 94 | /** 95 | * Statement constructor. 96 | * 97 | * @param \Doctrine\DBAL\Connection $connection 98 | * @param array|stdClass|\ArrayObject|object|string $entity 99 | */ 100 | public function __construct($connection = null, $entity = null) 101 | { 102 | $this->setConnection($connection); 103 | $this->setEntity($entity); 104 | } 105 | 106 | /** 107 | * Fetch data for entity 108 | * 109 | * @param string $option 110 | * @return array|\SplStack|\ArrayObject|bool 111 | * @throws \Doctrine\DBAL\DBALException 112 | */ 113 | public function execute($option = HydratorInterface::HYDRATE_AUTO) 114 | { 115 | //execute before events and proceed with builder from event 116 | $provider = $this->createProvider($this->getEntity()); 117 | $event = $this->beforeExecute($provider->getEntity()); 118 | 119 | if ($event->isCanceled()) { 120 | return false; 121 | } 122 | 123 | $builder = $event->getBuilder(); 124 | 125 | //convert entity to adapter again 126 | $provider = $this->createProvider($builder->getEntity()); 127 | 128 | $connection = $this->getConnection(); 129 | $isSelect = $builder->getType() === QueryBuilder::SELECT; 130 | 131 | $sql = $this->getSQL(); 132 | 133 | $statement = $isSelect ? 134 | //execute query and receive a statement 135 | $connection->executeQuery($sql, $this->getParameters(), $this->getParameterTypes()) : 136 | 137 | //execute query and receive a count of affected rows 138 | $connection->executeUpdate($sql, $this->getParameters(), $this->getParameterTypes()); 139 | 140 | //execute after events and proceed with result from event 141 | $event = $this->afterExecute( 142 | $isSelect ? 143 | $statement->fetchAll() : 144 | $statement, 145 | $provider->getEntity(), $builder); 146 | 147 | if ($event->isCanceled()) { 148 | return false; 149 | } 150 | 151 | $result = $this->convertTypesToPHPValues($provider, $event->getResult()); 152 | 153 | $data = (new EntityHydrator($provider))->hydrate($result, $option); 154 | gc_collect_cycles(); 155 | 156 | return $data; 157 | } 158 | 159 | /** 160 | * Emit events before query handling and if entity is able to emit events execute entity events 161 | * 162 | * @param $entity 163 | * @return QueryBuilderEvent 164 | */ 165 | private function beforeExecute($entity) 166 | { 167 | $builder = $this; 168 | $event = $this->getEmitter()->emit(new QueryBuilderEvent('build.' . $this->getTypeName(), $builder)); 169 | 170 | if ($entity instanceof EmitterAwareInterface) { 171 | $event = $entity->getEmitter()->emit($event); 172 | } 173 | 174 | return $event; 175 | } 176 | 177 | /** 178 | * Emit events after query handling and if entity is able to emit events execute entity events 179 | * 180 | * @param mixed $result Raw result 181 | * @param mixed $entity Entity which contains the events 182 | * @param Query $builder 183 | * @return QueryResultEvent 184 | */ 185 | private function afterExecute($result, $entity, $builder) 186 | { 187 | 188 | $event = $this->getEmitter()->emit(new QueryResultEvent('result.' . $builder->getTypeName(), $result), $builder); 189 | 190 | if ($entity instanceof EmitterAwareInterface) { 191 | $event = $entity->getEmitter()->emit($event, $builder); 192 | } 193 | 194 | return $event; 195 | } 196 | 197 | /** 198 | * Specifies an item that is to be returned in the query result. 199 | * Replaces any previously specified selections, if any. 200 | * 201 | * 202 | * $qb = $conn->createQueryBuilder() 203 | * ->select('u.id', 'p.id') 204 | * ->from('users', 'u') 205 | * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); 206 | * 207 | * 208 | * @param array $select The selection expressions. 209 | * 210 | * @return $this Query instance 211 | */ 212 | public function select(array $select = []) 213 | { 214 | if (count($select) < 1) { 215 | $select = ['*']; 216 | } 217 | 218 | return $this->__call(__FUNCTION__, [$select]); 219 | } 220 | 221 | /** 222 | * Magic call of \Doctrine\DBAL\Query\QueryBuilder methods 223 | * 224 | * @param string|callable $name 225 | * @param array $arguments 226 | * @return mixed 227 | */ 228 | public function __call($name, array $arguments = []) 229 | { 230 | $result = call_user_func_array([$this->getBuilder(), $name], $arguments); 231 | 232 | return $result instanceof QueryBuilder ? $this : $result; 233 | } 234 | 235 | /** 236 | * @return \Doctrine\DBAL\Query\QueryBuilder 237 | */ 238 | public function getBuilder() 239 | { 240 | if (null === $this->builder) { 241 | $this->builder = $this->getConnection()->createQueryBuilder(); 242 | } 243 | 244 | return $this->builder; 245 | } 246 | 247 | /** 248 | * @param \Doctrine\DBAL\Query\QueryBuilder $builder 249 | * 250 | * @return $this 251 | */ 252 | public function setBuilder(QueryBuilder $builder) 253 | { 254 | $this->builder = $builder; 255 | 256 | return $this; 257 | } 258 | 259 | /** 260 | * Get query type name 261 | * @return string 262 | * @throws \Exception 263 | */ 264 | public function getTypeName() 265 | { 266 | switch ($this->getType()) { 267 | case QueryBuilder::SELECT: 268 | return 'select'; 269 | case QueryBuilder::INSERT: 270 | return 'insert'; 271 | case QueryBuilder::UPDATE: 272 | return 'update'; 273 | case QueryBuilder::DELETE: 274 | return 'delete'; 275 | // @codeCoverageIgnoreStart 276 | default: 277 | //this could only happen if query will be extended and a custom getType is return invalid type 278 | throw new \Exception('Unknown query type ' . $this->getType()); 279 | } 280 | // @codeCoverageIgnoreEnd 281 | } 282 | 283 | /** 284 | * @param $provider 285 | * @param $result 286 | * @return mixed 287 | * @throws \Doctrine\DBAL\DBALException 288 | */ 289 | private function convertTypesToPHPValues(ProviderInterface $provider, $result) 290 | { 291 | if(!is_array($result)){ 292 | return $result; 293 | } 294 | $fields = $provider->getDefinition()->getFields(); 295 | 296 | foreach ($result as $index => $items) { 297 | foreach ($items as $key => $value) { 298 | $defaultType = Type::getType(is_numeric($value) ? Type::INTEGER : Type::STRING); 299 | $type = array_key_exists($key, $fields) ? $fields[$key]->getType() : $defaultType; 300 | $result[$index][$key] = $type->convertToPHPValue( 301 | $value, $this->getConnection()->getDatabasePlatform() 302 | ); 303 | } 304 | } 305 | 306 | return $result; 307 | } 308 | 309 | /** 310 | * Add a value for column on update or insert statement 311 | * 312 | * @param $column 313 | * @param $value 314 | * 315 | * @return $this 316 | */ 317 | public function addColumnValue($column, $value){ 318 | switch($this->getType()){ 319 | case QueryBuilder::INSERT: 320 | $this->setValue($column, $value); 321 | break; 322 | case QueryBuilder::UPDATE: 323 | $this->set($column, $value); 324 | break; 325 | } 326 | 327 | return $this; 328 | } 329 | 330 | } 331 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blast ORM 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Software License][ico-license]](LICENSE.md) 5 | [![Build Status][ico-travis]][link-travis] 6 | [![Total Downloads][ico-downloads]][link-downloads] 7 | [![Coverage Status][ico-coveralls]][link-coveralls] 8 | 9 | Framework agnostic data access and persistence based on Doctrine 2 DBAL. 10 | 11 | ## Features 12 | 13 | - Data and relation mapper _since 0.1_ 14 | - Decoupled entities as POPO's (Plain-old PHP objects) _since 0.3_ 15 | - Auto-Suggesting entity definition as well as configure custom definition _since 0.5_ 16 | - Data hydration to entity and vice versa _since 0.5_ 17 | - Repository contracted to a single entity class _since 0.5_ 18 | - Integration of fields from definition _since 0.5.2_ 19 | - Field type aware converting _since 0.5.2_ 20 | - Integration of indexes from definition _since 0.6.4_ 21 | - entity independent and connection dependent table prefixes _since 0.6.4_ 22 | - entity reflection and meta data caching _since 0.6.4_ 23 | 24 | ## Upcoming features 25 | 26 | - Unit of Work - Entity-aware transactions 27 | - Identity map - Reduce load by storing entity by primary key 28 | 29 | ## Install 30 | 31 | ### Using Composer 32 | 33 | Blast ORM is available on [Packagist](https://packagist.org/packages/blast/orm) and can be installed using [Composer](https://getcomposer.org/). This can be done by running the following command or by updating your `composer.json` file. 34 | 35 | ```bash 36 | composer require blast/orm 37 | ``` 38 | 39 | composer.json 40 | 41 | ```json 42 | { 43 | "require": { 44 | "blast/orm": "~1.0" 45 | } 46 | } 47 | ``` 48 | 49 | Be sure to also include your Composer autoload file in your project: 50 | 51 | ```php 52 | = 5.5.9 66 | * PHP 5.6 67 | * PHP 7.0 68 | * HHVM 69 | 70 | ## Example 71 | 72 | An example can be found in this [blog post](http://bit.ly/php-orm). 73 | 74 | ## Concept 75 | 76 | ### Entities 77 | 78 | An entity object is an in-memory representations of a database entity. Entity object are plain objects (aka POPO). 79 | It is recommended to use accessors (getters) and mutators (setters) for properties on plain objects. 80 | 81 | #### Provider 82 | 83 | The provider is a link between independent data entity and data access. The provider is also able to hydrate data to 84 | entity object and extract data from entity object. 85 | 86 | #### Definition 87 | 88 | Definition managing entity meta and schema specific configuration. Definition could be passed to mapper or provider, 89 | instead of an entity. 90 | 91 | ### Mappers 92 | 93 | Each entity does have it's own mapper. A mapper is determined by the entity provider. Mappers mediate between dbal 94 | and entity and provide convenient CRUD (Create, Read, Update, Delete). In addition to CRUD, the mapper is also delivering 95 | convenient methods to to work with relations. 96 | 97 | ### Query 98 | 99 | The query acts as accessor to the persistence layer. The query class is hydrating data on execution and transforms the 100 | result into a single entity class or `\ArrayObject` as fallback, a collection of entity classes as `\SplStack` or as a 101 | raw data array. Furthermore the query is able to receive hydration options to control the result. Create, delete and 102 | update are always returning a numeric value! 103 | 104 | ### Repository 105 | 106 | The repository is mediating between persistence layer and abstract from persistence or data access through mapper or query. 107 | Blast orm is just delivering a `Blast\Orm\RepositoryInterface` for completion! 108 | 109 | ## Usage 110 | 111 | ### Connections 112 | 113 | Blast ORM is managing all connections with `\Blast\Orm\ConnectionManager`. You are able to create connections directly 114 | or add connections to manager cache and access them later on. 115 | 116 | #### Direct access 117 | 118 | Create a new connection 119 | 120 | ```php 121 | add('mysql://root:root@localhost/defaultdb?charset=UTF-8', 'myconnectionname'); 152 | ``` 153 | 154 | Get connection, default connection name is always `default` 155 | 156 | ```php 157 | get(); 161 | 162 | //get connection by name 163 | $anotherConnection = $connections->get('another'); 164 | ``` 165 | 166 | Swap default connection with another connection. 167 | 168 | ```php 169 | setDefaultConnection('another'); 171 | 172 | ``` 173 | 174 | ### Query 175 | 176 | The query object is is providing high level API methods of [doctrine 2 query builder](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/query-builder.html#security-safely-preventing-sql-injection). 177 | 178 | Create query from connection 179 | 180 | ```php 181 | createQuery(); 184 | ``` 185 | 186 | with entity 187 | 188 | ```php 189 | createQuery(Post::class); 192 | ``` 193 | 194 | with definition 195 | 196 | ```php 197 | createQuery($definition); 200 | 201 | ``` 202 | 203 | Create a new query instance, the query is automatically determining current active connection from connection manager. 204 | 205 | ```php 206 | setConnection(ConnectionManager::create('mysql://root:root@localhost/acme')); 242 | 243 | ``` 244 | 245 | Custom query builder 246 | 247 | ```php 248 | setBuilder($connection->createQueryBuilder()); 251 | 252 | ``` 253 | 254 | #### select 255 | 256 | Get all posts as collection `\SplStack` containing post as `\ArrayObject` 257 | 258 | ```php 259 | select()->from('post', 'p')->execute(); 262 | 263 | //get data from result 264 | $title = $result['title']; 265 | $content = $result['content']; 266 | ``` 267 | 268 | Get post by id as `\ArrayObject` 269 | 270 | ```php 271 | select() 275 | ->from('post', 'p') 276 | ->where('id = :id') 277 | ->setParameter('id', $id) 278 | ->execute(); 279 | 280 | //loop results and get data 281 | foreach($results as $result){ 282 | $title = $result['title']; 283 | $content = $result['content']; 284 | } 285 | 286 | ``` 287 | 288 | #### create 289 | 290 | Create a new entry and get number of affected rows 291 | 292 | ```php 293 | insert('post') 296 | ->setValue('title', 'New Blog post') 297 | ->setValue('content', 'some blog content') 298 | ->execute(); 299 | ``` 300 | 301 | #### update 302 | 303 | Update an entry and get number of affected rows 304 | 305 | ```php 306 | update('post') 309 | ->set('title', 'New Blog post') 310 | ->where('id = :id') 311 | ->setParameter('id', 1) 312 | ->execute(); 313 | ``` 314 | 315 | #### delete 316 | 317 | Delete entries and get number of affected rows 318 | 319 | ```php 320 | delete('post') 323 | ->where('id = :id') 324 | ->setParameter('id', 1) 325 | ->execute(); 326 | ``` 327 | 328 | #### Advanced execution for select 329 | 330 | Execute query and get result as entity 331 | 332 | ```php 333 | execute(\Blast\Orm\Hydrator\HydratorInterface::HYDRATE_ENTITY); 336 | ``` 337 | 338 | Execute query and get result as collection 339 | 340 | ```php 341 | execute(\Blast\Orm\Hydrator\HydratorInterface::HYDRATE_COLLECTION); 344 | ``` 345 | 346 | Execute query and get raw result as array 347 | 348 | ```php 349 | execute(\Blast\Orm\Hydrator\HydratorInterface::HYDRATE_RAW); 352 | ``` 353 | 354 | #### Events 355 | 356 | Query is able to execute events for each statement type. 357 | 358 | - select 359 | - insert 360 | - update 361 | - delete 362 | 363 | ##### build.{type} 364 | 365 | Fire this event before query executes statement. 366 | 367 | ```php 368 | getEmitter()->addListener('build.select', function (QueryBuilderEvent $event) { 373 | $event->getBuilder()->setEntity(Post::class); 374 | }); 375 | 376 | $result = $query->select()->from('post')->where('id = 1')->execute(); 377 | 378 | ``` 379 | 380 | ##### result.{type} 381 | 382 | Fire this event after query executes statement and receives result. 383 | 384 | ```php 385 | getEmitter()->addListener('result.select', function (QueryResultEvent $event, Query $builder) { 391 | $result = $event->getResult(); 392 | 393 | foreach ($result as $key => $value) { 394 | $result[$key]['contentSize'] = strlen($value['content']); 395 | } 396 | 397 | $event->setResult($result); 398 | }); 399 | 400 | $result = $query->select()->from('post')->where('id = 1')->execute(); 401 | 402 | ``` 403 | 404 | ###### Canceling query execution 405 | 406 | Use `setCancel()` method from given event to cancel a query execution. 407 | 408 | On build a query statement 409 | 410 | ```php 411 | getEmitter()->addListener('build.select', function (QueryBuilderEvent $event) { 416 | $event->setCanceled(true); 417 | }); 418 | ``` 419 | 420 | On result a query statement 421 | 422 | ```php 423 | getEmitter()->addListener('result.select', function (QueryResultEvent $event) { 428 | $event->setCanceled(true); 429 | }); 430 | ``` 431 | 432 | ### Entities 433 | 434 | Entity classes are independent of Blast ORM. Entity fields are translated to underscore for database field mapping. 435 | Database fields are translated to camelcase for entity field mapping. 436 | 437 | ```php 438 | setConfiguration([ 460 | 'tableName' => 'user_role' 461 | ]); 462 | 463 | //from mapper 464 | $mapper = new Mapper($definition); 465 | 466 | //from query 467 | $query = new Query($connection, $definition); 468 | 469 | ``` 470 | 471 | A list of possible configuration 472 | 473 | ```php 474 | \ArrayObject::class, 478 | 'entityCollection' => \SplStack::class, 479 | 'events' => [], 480 | 'fields' => [], 481 | 'indexes' => [], 482 | 'primaryKeyName' => ProviderInterface::DEFAULT_PRIMARY_KEY_NAME, 483 | 'tableName' => '', 484 | 'mapper' => Mapper::class, 485 | 'relations' => [] 486 | ]; 487 | $definition->setConfiguration($configuration); 488 | ``` 489 | 490 | A definition could also be declared on an entity as FQCN by property 491 | 492 | ```php 493 | hasOne($entity, 'otherTable') 657 | ]; 658 | } 659 | } 660 | 661 | ``` 662 | 663 | ##### Access definition from provider 664 | 665 | Adapters grant access to data and definition, even if your entity class does not have definitions at all. 666 | 667 | ```php 668 | getTableName(); 683 | 684 | ``` 685 | 686 | Get primary key name 687 | 688 | ```php 689 | getPrimaryKeyName(); 691 | 692 | ``` 693 | 694 | Get entities mapper 695 | 696 | ```php 697 | getMapper(); 699 | 700 | ``` 701 | 702 | Get entities relation 703 | 704 | ```php 705 | getRelations(); 707 | 708 | ``` 709 | 710 | Hydrate data as array to entity 711 | 712 | ```php 713 | hydrate(['title' => 'Hello World']); 715 | 716 | ``` 717 | 718 | Hydrate data as entity to array 719 | 720 | ```php 721 | extract(); 723 | 724 | ``` 725 | 726 | ### Mapper 727 | 728 | The mapper prepares queries for data persistence and access of a provided entity class. All methods always return a query 729 | instance and need to execute manually. It is also possible to add event listeners for query 730 | 731 | #### Create a new mapper for entity 732 | 733 | Get entity specific mapper from connection 734 | 735 | ```php 736 | getMapper($post); 739 | 740 | ``` 741 | 742 | Get entity specific mapper from provider 743 | 744 | ```php 745 | getDefinition()->getMapper(); 751 | 752 | ``` 753 | 754 | #### find 755 | 756 | Fetch one entry by primary key 757 | 758 | ```php 759 | find(1)->execute(); 762 | 763 | ``` 764 | 765 | #### select 766 | 767 | Custom select query 768 | 769 | ```php 770 | select() 773 | ->where('title = "Hello world"') 774 | ->setMaxResults(1) 775 | ->execute(EntityHydratorInterface::RESULT_ENTITY); 776 | ``` 777 | 778 | #### delete 779 | 780 | `delete` expects an primary key or an array of primary keys. 781 | 782 | Delete onw entry 783 | 784 | ```php 785 | delete(1); 788 | ``` 789 | 790 | Delete many entries 791 | 792 | ```php 793 | delete([1, 2]); 796 | ``` 797 | 798 | ### Relations 799 | 800 | Relations provide access to related, parent and child entity from another entity. 801 | 802 | #### Passing relations 803 | 804 | Pass relations as `array` by computed static relation method in entity class. Relations are automatically mapped to entity. 805 | 806 | ```php 807 | comments; 817 | } 818 | 819 | public static function relation(Post $entity, Mapper $mapper){ 820 | return [ 821 | $mapper->hasMany($entity, Comments::class) 822 | ]; 823 | } 824 | } 825 | 826 | $post = $mapper->find(1); 827 | $comments = $post->getComments()->execute(); 828 | 829 | ``` 830 | 831 | You could also extend the relation query with `RelationInterface::getQuery`. 832 | 833 | #### HasOne (one-to-one) 834 | 835 | One entity is associated with one related entity by a field which associates with primary key in current entity. 836 | 837 | - `$entity` - Current entity instance 838 | - `$foreignEntity` - Entity class name, instance or table name 839 | - `$foreignKey` - Field name on related entity. `null` by default. Empty foreign key is determined by current entity table name and primary key name as follows: `{tableName}_{primaryKeyName}`, e.g `user_id` 840 | 841 | ##### Example 842 | 843 | One user has one address. 844 | 845 | ```php 846 | hasOne($user, Address::class, 'user_id'); 849 | 850 | ``` 851 | 852 | #### HasMany (one-to-many) 853 | 854 | One entity is associated with many related entities by a field which associates with primary key in current entity. 855 | 856 | - `$entity` - Current entity instance 857 | - `$foreignEntity` - Entity class name, instance or table name 858 | - `$foreignKey` - Field name on a related entity. `null` by default. Empty foreign key is determined by current entity table name and primary key name as follows: `{tableName}_{primaryKeyName}`, e.g `post_id` 859 | 860 | ##### Example 861 | 862 | One post has many comments 863 | 864 | ```php 865 | hasMany($post, Comments::class, 'post_id'); 868 | 869 | ``` 870 | 871 | #### BelongsTo (one-to-one or one-to-many) 872 | 873 | BelongsTo is the inverse of a HasOne or a HasMany relation. 874 | 875 | One entity is associated with one related entity by a field which associates with primary key in related entity. 876 | 877 | - `$entity` - Current entity instance 878 | - `$foreignEntity` - Entity class name, instance or table name 879 | - `$localKey` - Field name on current entity. `null` by default. Empty local key is determined by related entity table name and primary key name as follows: `{tableName}_{primaryKeyName}`, e.g `post_id` 880 | 881 | ##### Example 882 | 883 | One post has one or many comments 884 | 885 | ```php 886 | belongsTo($comment, Post::class, 'post_id'); 889 | 890 | ``` 891 | 892 | #### ManyToMany (many-to-many) 893 | 894 | Many entities of type _A_ are associated with many related entities of type _B_ by a junction table. The junction table 895 | stores associations from entities of type _A_ to entities of type _B_. 896 | 897 | - `$entity`: Current entity instance 898 | - `$foreignEntity`: Entity class name, instance or table name 899 | - `$foreignKey` - Field name on a related entity. `null` by default. Empty foreign key is determined by current primary key name. 900 | - `$localKey`: Field name on current entity. `null` by default. Empty foreign key is determined by related entity primary key name. 901 | - `$junction`: Junction table name. `null` by default. Empty table name is determined by entity table name and foreign entity table name as follows: `{tableName}_{foreignTableName}`, e.g `post_comment`. 902 | - `$junctionLocalKey`: Field name on a related entity. `null` by default. Empty junction local key is determined by current entity table name and primary key name as follows: `{tableName}_{primaryKeyName}`, e.g `post_id`. 903 | - `$junctionForeignKey`: Field name on current entity. `null` by default. Empty junction foreign key is determined by related entity table name and primary key name as follows: `{tableName}_{primaryKeyName}`, e.g `comment_id`. 904 | 905 | ##### Example 906 | 907 | One user has many roles, and one role has many users. Users primary key name is `id`, Roles primary key name is `pk` 908 | (Primary key short name). The junction table `user_role` contains `user_id` and `role_id` columns. 909 | 910 | ```php 911 | manyToMany($user, Role::class, 'pk', 'id', 'user_role', 'user_id', 'role_id'); 914 | 915 | ``` 916 | 917 | ### Repository 918 | 919 | The repository abstracts methods for data persistence and access. All methods execute their queries directly. 920 | 921 | Blast ORM provides a repository interface `\Blast\Orm\RepositoryInterface`. 922 | 923 | A repository knows it's entity. Therefore we need to pass the entity as class name or instance 924 | 925 | Create from interface 926 | 927 | ```php 928 | createMapper($this->getEntity())->select()->execute(HydratorInterface::HYDRATE_COLLECTION); 955 | } 956 | 957 | /** 958 | * Find entity by primary key 959 | * 960 | * @param mixed $primaryKey 961 | * @return \ArrayObject|\stdClass|object 962 | */ 963 | public function find($primaryKey) 964 | { 965 | return $this->createMapper($this->getEntity())->find($primaryKey)->execute(HydratorInterface::HYDRATE_ENTITY); 966 | } 967 | 968 | /** 969 | * Save new or existing entity data 970 | * 971 | * @param object|array $data 972 | * @return int|bool 973 | */ 974 | public function save($data) 975 | { 976 | return $this->createMapper($data)->save($data)->execute(); 977 | } 978 | 979 | } 980 | 981 | ``` 982 | 983 | Create repository by abstract 984 | 985 | ```php 986 | setEntity(Post::class); 999 | } 1000 | } 1001 | ``` 1002 | 1003 | Create repository instance 1004 | 1005 | ```php 1006 | find(1); 1020 | 1021 | ``` 1022 | 1023 | #### all 1024 | 1025 | Fetch all entries and return as collection `Blast\Orm\Data\DataObject` 1026 | 1027 | ```php 1028 | all(); 1031 | 1032 | foreach($posts as $post){ 1033 | 1034 | //do something 1035 | 1036 | } 1037 | 1038 | ``` 1039 | 1040 | #### save 1041 | 1042 | Save is determining if the entity is new and executes `Blast\Orm\Mapper::update` or if it is new `Blast\Orm\Mapper::create`. 1043 | 1044 | ```php 1045 | save($post); 1055 | ``` 1056 | 1057 | ## Further development 1058 | 1059 | Please visit our [milestones](https://github.com/phpthinktank/blast-orm/milestones) 1060 | 1061 | ## Change log 1062 | 1063 | Please see [CHANGELOG](ChangeLog-0.1.md) for more information what has changed recently. 1064 | 1065 | ## Testing 1066 | 1067 | ``` bash 1068 | $ composer test 1069 | ``` 1070 | 1071 | ## Contributing 1072 | 1073 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 1074 | 1075 | ## Security 1076 | 1077 | If you discover any security related issues, please email instead of using the issue tracker. 1078 | 1079 | ## Credits 1080 | 1081 | - [Marco Bunge][link-author] 1082 | - [All contributors][link-contributors] 1083 | 1084 | ## License 1085 | 1086 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 1087 | 1088 | [ico-version]: https://img.shields.io/packagist/v/blast/orm.svg?style=flat-square 1089 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 1090 | [ico-travis]: https://img.shields.io/travis/phpthinktank/blast-orm/master.svg?style=flat-square 1091 | [ico-downloads]: https://img.shields.io/packagist/dt/blast/orm.svg?style=flat-square 1092 | [ico-coveralls]: https://img.shields.io/coveralls/phpthinktank/blast-orm/master.svg?style=flat-square)](https://coveralls.io/github/phpthinktank/blast-orm?branch=master 1093 | 1094 | [link-packagist]: https://packagist.org/packages/blast/orm 1095 | [link-travis]: https://travis-ci.org/phpthinktank/blast-orm 1096 | [link-downloads]: https://packagist.org/packages/blast/orm 1097 | [link-author]: https://github.com/mbunge 1098 | [link-contributors]: ../../contributors 1099 | [link-coveralls]: https://coveralls.io/github/phpthinktank/blast-orm 1100 | --------------------------------------------------------------------------------