├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── README.md ├── composer.json ├── examples ├── README.md ├── basic_usage_example.php ├── custom_hydrator_example.php ├── custom_type_example.php └── db.db ├── phpunit.xml ├── src ├── Driver │ ├── Connection.php │ ├── ConnectionManager.php │ ├── ConnectionOptions.php │ ├── Cursor.php │ ├── GenericRepository.php │ ├── HydratingCursor.php │ ├── MemorySavingCursor.php │ ├── MongoDB │ │ ├── Connection.php │ │ ├── ConnectionOptions.php │ │ ├── Cursor.php │ │ ├── Id.php │ │ └── Repository.php │ └── Pdo │ │ ├── Connection.php │ │ ├── ConnectionOptions.php │ │ ├── Cursor.php │ │ ├── Id.php │ │ └── Repository.php ├── EntityManager.php ├── Enum.php ├── Exception │ ├── CollectionException.php │ ├── ConnectionException.php │ ├── CursorException.php │ ├── DriverException.php │ ├── HydratorException.php │ ├── IdentityMapException.php │ ├── MappingException.php │ ├── RepositoryException.php │ ├── SchemaException.php │ ├── StorageException.php │ ├── UnitOfWorkException.php │ └── VersionException.php ├── Hydration │ ├── GenericHydrator.php │ ├── HydratorAutoGenerate.php │ ├── HydratorFactory.php │ ├── MemorySavingHydrator.php │ └── ObjectHydrator.php ├── Id.php ├── Id │ ├── AutoGenerateId.php │ ├── CompositeId.php │ ├── GenericId.php │ └── Uuid.php ├── Mapping │ ├── Annotation │ │ ├── EmbeddedEntity.php │ │ ├── Entity.php │ │ ├── Property.php │ │ └── Property │ │ │ ├── Date.php │ │ │ ├── DecimalNumber.php │ │ │ ├── Embed.php │ │ │ ├── Enum.php │ │ │ ├── FloatNumber.php │ │ │ ├── Id.php │ │ │ ├── IntegerNumber.php │ │ │ ├── Reference.php │ │ │ └── Text.php │ ├── Collection.php │ ├── Collection │ │ ├── Collection.php │ │ └── LazyCollection.php │ ├── IdentityMap.php │ ├── MappingStrategy.php │ ├── MetaData │ │ ├── EntityMetaData.php │ │ ├── MetaDataFactory.php │ │ ├── PropertyMetaData.php │ │ └── Strategy │ │ │ └── AnnotationMetaDataFactory.php │ ├── Strategy │ │ ├── Date.php │ │ ├── DecimalNumber.php │ │ ├── DefaultAttributesProvider.php │ │ ├── Embed.php │ │ ├── Enum.php │ │ ├── FloatNumber.php │ │ ├── Id.php │ │ ├── IntegerNumber.php │ │ ├── Reference.php │ │ └── Text.php │ └── Type.php ├── Migration.php ├── Migration │ ├── Version.php │ └── VersionSynchronizer.php ├── MigrationManager.php ├── Repository.php ├── Storable.php ├── Storage.php └── UnitOfWork.php └── tests ├── Fixtures ├── Album │ ├── AlbumEntity.php │ ├── AlbumHydrator.php │ └── AlbumRepository.php ├── Artist │ ├── ArtistEntity.php │ ├── ArtistHydrator.php │ └── ArtistRepository.php ├── Genre │ ├── GenreEntity.php │ └── GenreRepository.php ├── Playlist │ ├── PlaylistDetails.php │ ├── PlaylistDetailsHydrator.php │ ├── PlaylistEntity.php │ └── PlaylistRepository.php ├── Track │ ├── TrackEntity.php │ └── TrackRepository.php ├── test.db └── test.json ├── Functional └── Storage │ ├── Driver │ ├── MongoDB │ │ ├── ConnectionTest.php │ │ ├── IdTest.php │ │ └── RepositoryTest.php │ └── PDO │ │ ├── CursorTest.php │ │ └── RepositoryTest.php │ ├── Hydration │ └── HydratorFactoryTest.php │ ├── Id │ └── UuidTest.php │ ├── Mapping │ ├── Collection │ │ ├── CollectionTest.php │ │ └── LazyCollectionTest.php │ ├── MetaData │ │ ├── EntityMetaDataTest.php │ │ ├── PropertyMetaDataTest.php │ │ └── Strategy │ │ │ └── AnnotationMetaDataFactoryTest.php │ ├── Strategy │ │ ├── DateTest.php │ │ ├── DecimalNumberTest.php │ │ ├── EmbedTest.php │ │ ├── EnumTest.php │ │ ├── FloatNumberTest.php │ │ ├── IntegerNumberTest.php │ │ └── TextTest.php │ └── TypeTest.php │ ├── MigrationManagerTest.php │ ├── StorageTest.php │ └── StorageTrait.php ├── Unit └── Storage │ └── Migration │ └── VersionTest.php ├── bootstrap.php └── tmp └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /composer.lock 3 | /.idea 4 | /vendor 5 | /tests/cache/* 6 | /tests/tmp/* 7 | /metrics 8 | /coverage.clover 9 | /coverage 10 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | php: 3 | code_rating: true 4 | 5 | build: 6 | tests: 7 | override: 8 | command: "php -v" 9 | 10 | tools: 11 | external_code_coverage: true 12 | php_analyzer: true 13 | php_changetracking: true 14 | php_code_sniffer: 15 | config: 16 | standard: "PSR2" 17 | php_mess_detector: true 18 | 19 | filter: 20 | excluded_paths: 21 | - docs/* 22 | - tests/* 23 | - src/Exception/* 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: true 4 | 5 | php: 6 | - 7.1 7 | 8 | services: 9 | - mongodb 10 | - mysql 11 | 12 | before_install: 13 | - composer self-update 14 | - mysql -e "CREATE SCHEMA test; GRANT ALL PRIVILEGES ON test.* to travis@'%'" 15 | - pecl install mongodb 16 | - mongo test --eval 'db.createUser({user:"travis",pwd:"test",roles:["readWrite"]});' 17 | 18 | install: 19 | - composer update 20 | 21 | script: 22 | - vendor/bin/phpunit 23 | 24 | after_script: 25 | - wget https://scrutinizer-ci.com/ocular.phar 26 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 27 | 28 | cache: 29 | directories: 30 | - $HOME/.composer/cache 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "igniphp/storage", 3 | "description": "Minimalistic entity framework with multi database support.", 4 | "keywords": [ 5 | "ODM", 6 | "ORM", 7 | "Entity framework", 8 | "Unit of work", 9 | "Identity map", 10 | "Mysql", 11 | "Sqlite", 12 | "Mongo", 13 | "Pgsql", 14 | "php71" 15 | ], 16 | "license": "MIT", 17 | "authors": [ 18 | { 19 | "name": "Dawid Kraczkowski", 20 | "email": "dawid.kraczkowski@gmail.com" 21 | } 22 | ], 23 | "suggest": { 24 | "ext-mongodb": "For mongodb support", 25 | "ext-pdo_mysql": "For MySQL or MariaDB database support", 26 | "ext-pdo_pqsql": "For PostgreSQL database support", 27 | "ext-sqlite3": "For SQLite database support", 28 | "ext-base58": "For better uuid generation performance" 29 | }, 30 | "require": { 31 | "php": ">=7.1.0", 32 | "ext-pdo": "*", 33 | "ext-hash": "*", 34 | "ext-json": "*", 35 | "psr/simple-cache": ">=1.0.0", 36 | "igniphp/exception": ">=1.0.0", 37 | "igniphp/reflection-api": ">=1.0.2", 38 | "igniphp/uuid": ">=2.0.0", 39 | "doctrine/annotations": ">=1.6", 40 | "cache/array-adapter": ">=1.0", 41 | "cache/apcu-adapter": ">=1.0", 42 | "cache/apc-adapter": ">=1.0" 43 | }, 44 | "require-dev": { 45 | "phpunit/phpunit": ">=5.7.0", 46 | "mockery/mockery": ">=0.9.4", 47 | "phpunit/php-code-coverage": ">=4.0.0", 48 | "phpstan/phpstan": ">=0.9.2" 49 | }, 50 | "autoload": { 51 | "psr-4": { 52 | "Igni\\Storage\\": "src/" 53 | } 54 | }, 55 | "autoload-dev": { 56 | "psr-4": { 57 | "Igni\\Tests\\": "tests/" 58 | } 59 | }, 60 | "scripts": { 61 | "phpstan": "vendor/bin/phpstan analyse src --level=0", 62 | "coverage": "vendor/bin/phpunit --coverage-html ../coverage" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # ![Igni logo](https://github.com/igniphp/common/blob/master/logo/full.svg) 2 | 3 | This directory contains example usage of storage framework and example sqlite database. 4 | -------------------------------------------------------------------------------- /examples/basic_usage_example.php: -------------------------------------------------------------------------------- 1 | title = $title; 38 | $this->artist = $artist; 39 | } 40 | } 41 | 42 | /** 43 | * Records are taken from `artists` table and hydrated to `Artist` instance 44 | * @Entity(source="artists") 45 | */ 46 | class Artist implements Storable 47 | { 48 | use AutoGenerateId; 49 | 50 | /** @Property\Id(class=GenericId::class, name="ArtistId") */ 51 | public $id; 52 | 53 | /** @Property\Text() */ 54 | public $name; 55 | 56 | public function __construct(string $name) 57 | { 58 | $this->name = $name; 59 | } 60 | } 61 | 62 | // The following lines contain repositories 63 | 64 | /** 65 | * Repositories can be used to define complex queries and aggregations by using native SQL queries. 66 | */ 67 | class TrackRepository extends Repository 68 | { 69 | /** 70 | * Finds all tracks that belongs to given artist and return results as collection. 71 | */ 72 | public function findByArtist(Artist $artist): Collection 73 | { 74 | $cursor = $this->query(" 75 | SELECT tracks.* 76 | FROM tracks 77 | JOIN albums ON albums.AlbumId = tracks.AlbumId 78 | JOIN artists ON artists.ArtistId = albums.ArtistId 79 | WHERE albums.ArtistId = :id 80 | ", [ 81 | 'id' => $artist->getId() 82 | ]); 83 | 84 | return new Collection($cursor); 85 | } 86 | 87 | public static function getEntityClass(): string 88 | { 89 | return Track::class; 90 | } 91 | } 92 | // Work with unit of work 93 | // Define connections: 94 | ConnectionManager::registerDefault(new Connection('sqlite:/' . __DIR__ . '/db.db')); 95 | 96 | $storage = new Storage(); 97 | 98 | // Attach repositories 99 | $storage->addRepository( 100 | // Dynamic Repository 101 | new class($storage->getEntityManager()) extends Repository { 102 | public static function getEntityClass(): string 103 | { 104 | return Artist::class; 105 | } 106 | }, 107 | 108 | // Custom Repository class 109 | new TrackRepository($storage->getEntityManager()) 110 | 111 | ); 112 | 113 | // Fetch items from database 114 | 115 | $artist = $storage->get(Artist::class, 1); // This is equivalent to: SELECT *FROM artists WHERE ArtistId = 1 116 | $track = $storage->get(Track::class, 1); // This is equivalent to: SELECT *FROM tracks WHERE TrackId = 1 117 | 118 | // Iterate through all tracks that belong to given artist 119 | foreach ($storage->getRepository(Track::class)->findByArtist($artist) as $track) { 120 | echo $track->title; 121 | } 122 | 123 | // Create new artist. 124 | $jimmy = new Artist('Moaning Jimmy'); 125 | $storage->persist($jimmy); 126 | 127 | // Update track's artist. 128 | $track->artist = $jimmy; 129 | 130 | $storage->remove($artist); // This will remove existing artist with id 1 once commit is executed. 131 | 132 | $storage->persist($track); // Save changes that will be flushed to database once commit is executed. 133 | 134 | $storage->commit(); // All update queries will happen from this point on 135 | -------------------------------------------------------------------------------- /examples/custom_hydrator_example.php: -------------------------------------------------------------------------------- 1 | baseHydrator = $baseHydrator; 28 | } 29 | 30 | public function hydrate(array $data) 31 | { 32 | /** @var Track $entity */ 33 | $entity = $this->baseHydrator->hydrate($data); 34 | $entity->setAlbum('Unknown album'); 35 | // Here do custom hydration 36 | return $entity; 37 | } 38 | 39 | public function extract($entity): array 40 | { 41 | $data = $this->baseHydrator->extract($entity); 42 | // Here extract additional properties 43 | return $data; 44 | } 45 | } 46 | 47 | /** 48 | * This entity will be hydrated with our custom hydrator class. 49 | * Please take a careful look at the annotation below- it is using hydrator 50 | * property so framework knows which class should be used during the hydration. 51 | * 52 | * @Entity(source="tracks", hydrator=TrackHydrator::class) 53 | */ 54 | class Track implements Storable 55 | { 56 | /** 57 | * @Property(type="id", name="TrackId", class=GenericId::class) 58 | */ 59 | protected $id; 60 | 61 | /** 62 | * @Property(type="string", name="Name") 63 | */ 64 | protected $name; 65 | 66 | /** 67 | * Please note: that this property is not hydrated by generic hydrator. 68 | * It gets hydrated by TrackHydrator. 69 | */ 70 | protected $album; 71 | 72 | public function __construct(string $name) 73 | { 74 | $this->name = $name; 75 | } 76 | 77 | public function setAlbum(string $album) 78 | { 79 | $this->album = $album; 80 | } 81 | 82 | public function getAlbum() 83 | { 84 | return $this->album; 85 | } 86 | 87 | public function getId(): Id 88 | { 89 | return $this->id; 90 | } 91 | } 92 | 93 | // Below we setup bootstrap; connection and unit of work instance (Storage instance) 94 | 95 | ConnectionManager::registerDefault(new Connection('sqlite:/' . __DIR__ . '/db.db')); 96 | 97 | $unitOfWork = new Storage(); 98 | 99 | // Repository has to be registered so framework knows which class is responsible for obtaining given entity from database. 100 | $unitOfWork->addRepository(new class($unitOfWork->getEntityManager()) extends Repository { 101 | public static function getEntityClass(): string 102 | { 103 | return Track::class; 104 | } 105 | }); 106 | 107 | // Now database is queried and data gets hydrated with custom hydrator to instance of Track class. 108 | $track = $unitOfWork->get(Track::class, 1); 109 | 110 | echo $track->getAlbum();// Unknown album. 111 | -------------------------------------------------------------------------------- /examples/custom_type_example.php: -------------------------------------------------------------------------------- 1 | name = $name; 40 | } 41 | 42 | public function __toString(): string 43 | { 44 | return $this->name; 45 | } 46 | } 47 | 48 | /** 49 | * @Entity(source="tracks") 50 | */ 51 | class Track implements Storable 52 | { 53 | use AutoGenerateId; 54 | 55 | /** 56 | * @Property(type="id", name="TrackId", class=GenericId::class) 57 | */ 58 | protected $id; 59 | 60 | /** 61 | * @Property(type="string", name="Name") 62 | */ 63 | protected $name; 64 | 65 | /** 66 | * @Property(name="Composer", type="composer") 67 | */ 68 | protected $composer; 69 | 70 | public function __construct(string $name, Composer $composer) 71 | { 72 | $this->name = $name; 73 | $this->composer = $composer; 74 | } 75 | 76 | public function getComposer(): Composer 77 | { 78 | return $this->composer; 79 | } 80 | 81 | public function getName(): string 82 | { 83 | return $this->name; 84 | } 85 | } 86 | 87 | ConnectionManager::registerDefault(new Connection('sqlite:/' . __DIR__ . '/db.db')); 88 | $unitOfWork = new Storage(); 89 | $unitOfWork->addRepository(new class($unitOfWork->getEntityManager()) extends Repository { 90 | public static function getEntityClass(): string 91 | { 92 | return Track::class; 93 | } 94 | }); 95 | 96 | $track = $unitOfWork->get(Track::class, 1); 97 | 98 | print_r($track->getComposer());// Instance of composer. 99 | -------------------------------------------------------------------------------- /examples/db.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igniphp/storage/dbfa7f0a29a67f02b113efe448346f195180bcf8/examples/db.db -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | ./src/ 20 | 21 | ./src/Exception 22 | ./tests 23 | ./examples 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Driver/Connection.php: -------------------------------------------------------------------------------- 1 | close(); 18 | } 19 | 20 | self::$connections = []; 21 | } 22 | 23 | public static function register(string $name, Connection $connection): void 24 | { 25 | if (self::has($name)) { 26 | throw StorageException::forAlreadyExistingConnection($name); 27 | } 28 | 29 | self::$connections[$name] = $connection; 30 | } 31 | 32 | public static function registerDefault(Connection $connection): void 33 | { 34 | self::register(self::DEFAULT_NAME, $connection); 35 | } 36 | 37 | public static function has(string $name): bool 38 | { 39 | return isset(self::$connections[$name]); 40 | } 41 | 42 | public static function hasDefault(): bool 43 | { 44 | return self::has(self::DEFAULT_NAME); 45 | } 46 | 47 | public static function getDefault(): Connection 48 | { 49 | return self::get(self::DEFAULT_NAME); 50 | } 51 | 52 | public static function get(string $name): Connection 53 | { 54 | if (!self::has($name)) { 55 | throw StorageException::forNotRegisteredConnection($name); 56 | } 57 | 58 | return self::$connections[$name]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Driver/ConnectionOptions.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 33 | $this->metaData = $this->entityManager->getMetaData($this->getEntityClass()); 34 | $this->hydrator = $this->entityManager->getHydrator($this->getEntityClass()); 35 | if ($connection === null) { 36 | $connection = ConnectionManager::get($this->metaData->getConnection()); 37 | } 38 | $this->connection = $connection; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Driver/HydratingCursor.php: -------------------------------------------------------------------------------- 1 | host = $host; 28 | $this->options = $options; 29 | } 30 | 31 | public function close(): void 32 | { 33 | $this->handler = null; 34 | } 35 | 36 | public function connect(): void 37 | { 38 | $this->handler = new MongoDB\Driver\Manager( 39 | 'mongodb://' . $this->host . '/' . $this->options->getDatabase(), 40 | $this->options->getURIOptions(), 41 | $this->options->getDriverOptions() 42 | ); 43 | } 44 | 45 | public function isConnected(): bool 46 | { 47 | return $this->handler !== null; 48 | } 49 | 50 | public function createCursor(...$parameters): Cursor 51 | { 52 | if (!$this->isConnected()) { 53 | $this->connect(); 54 | } 55 | 56 | $command = new MongoDB\Driver\Command($parameters[0]); 57 | 58 | return new Cursor($this, $this->options, $command); 59 | } 60 | 61 | public function dropCollection(string $collection): void 62 | { 63 | $cursor = $this->createCursor([ 64 | 'drop' => $collection, 65 | ]); 66 | $cursor->execute(); 67 | } 68 | 69 | public function insert(string $collection, array ...$documents): void 70 | { 71 | $cursor = $this->createCursor([ 72 | 'insert' => $collection, 73 | 'documents' => $documents, 74 | ]); 75 | $cursor->execute(); 76 | } 77 | 78 | public function remove(string $collection, ...$ids): void 79 | { 80 | $deletes = []; 81 | foreach ($ids as $id) { 82 | $deletes[] = [ 83 | 'q' => [ 84 | '_id' => $id, 85 | ], 86 | 'limit' => 1, 87 | ]; 88 | } 89 | $cursor = $this->createCursor([ 90 | 'delete' => $collection, 91 | 'deletes' => $deletes, 92 | ]); 93 | $cursor->execute(); 94 | } 95 | 96 | public function find(string $collection, array $query = [], array $options = []): Cursor 97 | { 98 | if (!empty($options) && array_diff(array_keys($options), self::VALID_FIND_OPTIONS)) { 99 | throw DriverException::forOperationFailure('Invalid option passed to find query.'); 100 | } 101 | $command = array_merge([ 102 | 'find' => $collection, 103 | 'filter' => $query, 104 | ], $options); 105 | if (empty($command['filter'])) { 106 | unset ($command['filter']); 107 | } 108 | 109 | return $this->createCursor($command); 110 | } 111 | 112 | public function count(string $collection, array $query): Cursor 113 | { 114 | return $this->createCursor([ 115 | 'count' => $collection, 116 | 'query' => $query, 117 | ]); 118 | } 119 | 120 | public function update(string $collection, array ...$documents): void 121 | { 122 | $updates = []; 123 | foreach ($documents as $document) { 124 | $id = null; 125 | if (!isset($document['_id'])) { 126 | if (!isset($document['id'])) { 127 | throw DriverException::forOperationFailure('Cannot update documents without identity.'); 128 | } 129 | $id = $document['id']; 130 | unset($document['id']); 131 | } else { 132 | $id = $document['_id']; 133 | unset($document['_id']); 134 | } 135 | 136 | $updates[] = [ 137 | 'q' => [ 138 | '_id' => $id, 139 | ], 140 | 'u' => $document, 141 | 'upsert' => true, 142 | ]; 143 | } 144 | $cursor = $this->createCursor([ 145 | 'update' => $collection, 146 | 'updates' => $updates, 147 | ]); 148 | $cursor->execute(); 149 | } 150 | 151 | public function getBaseConnection(): MongoDB\Driver\Manager 152 | { 153 | return $this->handler; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Driver/MongoDB/ConnectionOptions.php: -------------------------------------------------------------------------------- 1 | 'primary', 11 | MongoDB\Driver\ReadPreference::RP_PRIMARY_PREFERRED => 'primaryPreferred', 12 | MongoDB\Driver\ReadPreference::RP_SECONDARY => 'secondary', 13 | MongoDB\Driver\ReadPreference::RP_SECONDARY_PREFERRED => 'secondaryPreferred', 14 | MongoDB\Driver\ReadPreference::RP_NEAREST => 'nearest', 15 | ]; 16 | /** @var string */ 17 | private $username; 18 | 19 | /** @var string */ 20 | private $password; 21 | 22 | /** @var string */ 23 | private $database; 24 | 25 | /** @var int */ 26 | private $connectTimeout; 27 | 28 | /** @var int */ 29 | private $socketTimeout; 30 | 31 | /** @var string */ 32 | private $appName; 33 | 34 | /** @var string */ 35 | private $replicaSet; 36 | 37 | /** @var string */ 38 | private $authMechanism; 39 | 40 | /** @var array */ 41 | private $authOptions; 42 | 43 | /** @var MongoDB\Driver\ReadConcern */ 44 | private $readConcern; 45 | 46 | /** @var MongoDB\Driver\WriteConcern */ 47 | private $writeConcern; 48 | 49 | /** @var MongoDB\Driver\ReadPreference */ 50 | private $readPreference; 51 | 52 | /** @var string */ 53 | private $sslPemFile; 54 | 55 | /** @var string */ 56 | private $sslPemPassword; 57 | 58 | /** @var resource */ 59 | private $sslContext; 60 | 61 | public function __construct(string $database, string $username = null, string $password = null) 62 | { 63 | $this->username = $username; 64 | $this->password = $password; 65 | $this->database = $database; 66 | $this->readPreference = new MongoDB\Driver\ReadPreference(MongoDB\Driver\ReadPreference::RP_PRIMARY); 67 | } 68 | 69 | public function setReadConcern(MongoDB\Driver\ReadConcern $concern): void 70 | { 71 | $this->readConcern = $concern; 72 | } 73 | 74 | public function setWriteConcern(MongoDB\Driver\WriteConcern $concern): void 75 | { 76 | $this->writeConcern = $concern; 77 | } 78 | 79 | public function setReadPreference(MongoDB\Driver\ReadPreference $preference): void 80 | { 81 | $this->readPreference = $preference; 82 | } 83 | 84 | public function setAppName(string $appName): void 85 | { 86 | $this->appName = $appName; 87 | } 88 | 89 | /** 90 | * Specifies non default authentication mechanism 91 | * @param string $mechanism 92 | * @param array $options 93 | * @see https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst#auth-related-options 94 | */ 95 | public function setAuth(string $mechanism, array $options = []): void 96 | { 97 | $this->authMechanism = $mechanism; 98 | $this->authOptions = $options; 99 | } 100 | 101 | public function setConnectionTimeout(int $milliseconds): void 102 | { 103 | $this->connectTimeout = $milliseconds; 104 | } 105 | 106 | public function setSocketTimeout(int $milliseconds): void 107 | { 108 | $this->socketTimeout = $milliseconds; 109 | } 110 | 111 | public function setReplicaSet(string $name): void 112 | { 113 | $this->replicaSet = $name; 114 | } 115 | 116 | public function useSSL(string $pemFile, string $pemPassword = null, array $context = null): void 117 | { 118 | if ($context !== null) { 119 | $this->sslContext = stream_context_create([ 120 | 'ssl' => $context 121 | ]); 122 | } 123 | 124 | $this->sslPemFile = $pemFile; 125 | $this->sslPemPassword = $pemPassword; 126 | } 127 | 128 | public function getURIOptions(): array 129 | { 130 | $options = []; 131 | if ($this->username !== null) { 132 | $options['username'] = $this->username; 133 | } 134 | 135 | if ($this->password !== null) { 136 | $options['password'] = $this->password; 137 | } 138 | 139 | if ($this->appName !== null) { 140 | $options['appname'] = $this->appName; 141 | } 142 | 143 | if ($this->authMechanism !== null) { 144 | $options['authMechanism'] = $this->authMechanism; 145 | if ($this->authOptions !== null) { 146 | $options['authMechanismProperties'] = $this->authOptions; 147 | } 148 | } 149 | 150 | if ($this->connectTimeout !== null) { 151 | $options['connectTimeoutMS'] = $this->connectTimeout; 152 | } 153 | 154 | if ($this->socketTimeout !== null) { 155 | $options['socketTimeoutMS'] = $this->socketTimeout; 156 | } 157 | 158 | if ($this->replicaSet !== null) { 159 | $options['replicaSet'] = $this->replicaSet; 160 | } 161 | 162 | if ($this->readConcern !== null) { 163 | $options['readConcernLevel'] = $this->readConcern->getLevel(); 164 | } 165 | 166 | if ($this->readPreference !== null) { 167 | if (method_exists($this->readPreference, 'getMaxStalenessSeconds')) { 168 | $options['maxStalenessSeconds'] = $this->readPreference->getMaxStalenessSeconds(); 169 | } 170 | $options['readPreference'] = self::READ_PREFERENCE[$this->readPreference->getMode()]; 171 | } 172 | 173 | return $options; 174 | } 175 | 176 | public function getDriverOptions(): array 177 | { 178 | if (!$this->sslPemFile) { 179 | return []; 180 | } 181 | $options = [ 182 | 'pem_file' => $this->sslPemFile, 183 | ]; 184 | 185 | if ($this->sslPemPassword) { 186 | $options['pem_pwd'] = $this->sslPemPassword; 187 | } 188 | 189 | if ($this->sslContext) { 190 | $options['context'] = $this->sslContext; 191 | } 192 | 193 | return $options; 194 | } 195 | 196 | public function getDatabase(): string 197 | { 198 | return $this->database; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Driver/MongoDB/Cursor.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 41 | $this->command = $command; 42 | $this->options = $options; 43 | } 44 | 45 | public function getId(): string 46 | { 47 | return (string) $this->baseCursor->getId(); 48 | } 49 | 50 | public function getBaseCursor(): \MongoDB\Driver\Cursor 51 | { 52 | $this->open(); 53 | return $this->baseCursor; 54 | } 55 | 56 | public function getConnection(): \Igni\Storage\Driver\Connection 57 | { 58 | return $this->connection; 59 | } 60 | 61 | public function hydrateWith(ObjectHydrator $hydrator): void 62 | { 63 | $this->hydrator = $hydrator; 64 | } 65 | 66 | public function saveMemory(bool $save = true): void 67 | { 68 | if ($this->hydrator instanceof MemorySavingHydrator) { 69 | $this->hydrator->saveMemory($save); 70 | } 71 | } 72 | 73 | public function current() 74 | { 75 | $this->open(); 76 | return $this->current; 77 | } 78 | 79 | public function next(): void 80 | { 81 | $this->open(); 82 | $this->iterator->next(); 83 | $this->current = $this->fetch(); 84 | } 85 | 86 | public function key(): int 87 | { 88 | $this->open(); 89 | return $this->iterator->key(); 90 | } 91 | 92 | public function valid(): bool 93 | { 94 | $this->open(); 95 | return $this->iterator->valid(); 96 | } 97 | 98 | public function rewind(): void 99 | { 100 | $this->close(); 101 | $this->open(); 102 | } 103 | 104 | public function toArray(): array 105 | { 106 | return iterator_to_array($this); 107 | } 108 | 109 | public function open(): void 110 | { 111 | if ($this->iterator) { 112 | return; 113 | } 114 | try { 115 | $this->baseCursor = $this->connection 116 | ->getBaseConnection() 117 | ->executeCommand( 118 | $this->options->getDatabase(), 119 | $this->command 120 | ); 121 | $this->baseCursor->setTypeMap(['root' => 'array', 'document' => 'array', 'array' => 'array']); 122 | $this->iterator = new IteratorIterator($this->baseCursor); 123 | $this->iterator->rewind(); 124 | if ($this->iterator->valid()) { 125 | $this->current = $this->fetch(); 126 | } 127 | } catch (\Exception $e) { 128 | throw CursorException::forExecutionFailure($this, $e->getMessage()); 129 | } 130 | } 131 | 132 | public function close(): void 133 | { 134 | $this->current = null; 135 | if ($this->baseCursor) { 136 | $this->baseCursor = null; 137 | $this->iterator = null; 138 | } 139 | } 140 | 141 | public function execute(): void 142 | { 143 | $this->open(); 144 | $this->close(); 145 | } 146 | 147 | private function fetch() 148 | { 149 | $fetched = $this->iterator->current(); 150 | if (isset($fetched['_id'])) { 151 | $fetched['id'] = $fetched['_id']; 152 | unset($fetched['_id']); 153 | } 154 | if ($this->hydrator && $fetched !== null) { 155 | $fetched = $this->hydrator->hydrate($fetched); 156 | } 157 | 158 | return $fetched; 159 | } 160 | 161 | public function __destruct() 162 | { 163 | $this->close(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Driver/MongoDB/Id.php: -------------------------------------------------------------------------------- 1 | value = $value; 22 | } 23 | 24 | public function getValue() 25 | { 26 | return $this->value; 27 | } 28 | 29 | public function __toString(): string 30 | { 31 | return (string) $this->value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Driver/MongoDB/Repository.php: -------------------------------------------------------------------------------- 1 | connection->find( 14 | $this->metaData->getSource(), 15 | ['_id' => $id], 16 | ['limit' => 1] 17 | ); 18 | $cursor->hydrateWith($this->hydrator); 19 | 20 | $entity = $cursor->current(); 21 | $cursor->close(); 22 | 23 | if (!$entity instanceof Storable) { 24 | throw RepositoryException::forNotFound($id); 25 | } 26 | 27 | return $entity; 28 | } 29 | 30 | public function create(Storable $entity): Storable 31 | { 32 | // Support id auto-generation. 33 | $entity->getId(); 34 | $data = $this->hydrator->extract($entity); 35 | if (isset($data['id'])) { 36 | $data['_id'] = $data['id']; 37 | unset($data['id']); 38 | } 39 | $this->connection->insert( 40 | $this->metaData->getSource(), 41 | $data 42 | ); 43 | 44 | return $entity; 45 | } 46 | 47 | public function remove(Storable $entity): Storable 48 | { 49 | $this->connection->remove( 50 | $this->metaData->getSource(), 51 | $entity->getId()->getValue() 52 | ); 53 | 54 | return $entity; 55 | } 56 | 57 | public function update(Storable $entity): Storable 58 | { 59 | $this->connection->update( 60 | $this->metaData->getSource(), 61 | $this->hydrator->extract($entity) 62 | ); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Driver/Pdo/Connection.php: -------------------------------------------------------------------------------- 1 | dsn = $dsn; 26 | $this->options = $options ?? new ConnectionOptions(); 27 | } 28 | 29 | public function close(): void 30 | { 31 | $this->handler = null; 32 | } 33 | 34 | public function connect(): void 35 | { 36 | if ($this->isConnected()) { 37 | return; 38 | } 39 | 40 | $this->handler = new PDO( 41 | $this->dsn, 42 | $this->options->getUsername(), 43 | $this->options->getPassword(), 44 | $this->options->getAttributes() 45 | ); 46 | } 47 | 48 | public function isConnected(): bool 49 | { 50 | return $this->handler !== null; 51 | } 52 | 53 | /** 54 | * @param string $query 55 | * @param array $parameters 56 | * @return Cursor 57 | * @throws ConnectionException 58 | */ 59 | public function createCursor(...$parameters): Cursor 60 | { 61 | if (!$this->isConnected()) { 62 | $this->connect(); 63 | } 64 | 65 | $query = $parameters[0]; 66 | 67 | if (isset($parameters[1]) && is_array($parameters[1])) { 68 | return new Cursor($this, $query, $parameters[1]); 69 | } 70 | 71 | return new Cursor($this, $query); 72 | } 73 | 74 | public function beginTransaction(): bool 75 | { 76 | return $this->handler->beginTransaction(); 77 | } 78 | 79 | public function inTransaction(): bool 80 | { 81 | return $this->handler->inTransaction(); 82 | } 83 | 84 | public function commit(): bool 85 | { 86 | return $this->handler->commit(); 87 | } 88 | 89 | public function rollBack(): bool 90 | { 91 | return $this->handler->rollBack(); 92 | } 93 | 94 | public function quote($string): string 95 | { 96 | return $this->handler->quote((string) $string); 97 | } 98 | 99 | public function getBaseConnection(): PDO 100 | { 101 | $this->connect(); 102 | return $this->handler; 103 | } 104 | 105 | public function log(string $query) 106 | { 107 | $this->queryLog[] = $query; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Driver/Pdo/ConnectionOptions.php: -------------------------------------------------------------------------------- 1 | username = $username; 17 | $this->password = $password; 18 | $this->databaseName = $databaseName; 19 | 20 | $this->usePersistentConnection(); 21 | $this->useExceptions(); 22 | } 23 | 24 | public function silenceErrors(): void 25 | { 26 | $this->attributes[PDO::ATTR_ERRMODE] = PDO::ERRMODE_SILENT; 27 | } 28 | 29 | public function useExceptions($use = true): void 30 | { 31 | if ($use) { 32 | $this->attributes[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; 33 | } else { 34 | $this->attributes[PDO::ATTR_ERRMODE] = PDO::ERRMODE_WARNING; 35 | } 36 | } 37 | 38 | public function usePersistentConnection($use = true): void 39 | { 40 | $this->attributes[PDO::ATTR_PERSISTENT] = $use; 41 | } 42 | 43 | public function isPersistentConnection(): bool 44 | { 45 | return isset($this->attributes[PDO::ATTR_PERSISTENT]) && $this->attributes[PDO::ATTR_PERSISTENT]; 46 | } 47 | 48 | public function getUsername(): string 49 | { 50 | return $this->username; 51 | } 52 | 53 | public function getPassword(): string 54 | { 55 | return $this->password; 56 | } 57 | 58 | public function getAttributes(): array 59 | { 60 | return $this->attributes; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Driver/Pdo/Cursor.php: -------------------------------------------------------------------------------- 1 | connection = $connection; 35 | $this->query = $query; 36 | $this->params = $params; 37 | } 38 | 39 | public function getBaseCursor(): \PDOStatement 40 | { 41 | $this->open(); 42 | return $this->baseCursor; 43 | } 44 | 45 | public function getConnection(): \Igni\Storage\Driver\Connection 46 | { 47 | return $this->connection; 48 | } 49 | 50 | public function hydrateWith(ObjectHydrator $hydrator): void 51 | { 52 | $this->hydrator = $hydrator; 53 | } 54 | 55 | public function saveMemory(bool $save = true): void 56 | { 57 | if ($this->hydrator instanceof MemorySavingHydrator) { 58 | $this->hydrator->saveMemory($save); 59 | } 60 | } 61 | 62 | public function current() 63 | { 64 | $this->open(); 65 | return $this->current; 66 | } 67 | 68 | public function next(): void 69 | { 70 | $this->open(); 71 | $this->iterator->next(); 72 | $this->current = $this->fetch(); 73 | } 74 | 75 | public function key(): int 76 | { 77 | $this->open(); 78 | return $this->iterator->key(); 79 | } 80 | 81 | public function valid(): bool 82 | { 83 | $this->open(); 84 | return $this->iterator->valid(); 85 | } 86 | 87 | public function rewind(): void 88 | { 89 | $this->close(); 90 | $this->open(); 91 | } 92 | 93 | public function close(): void 94 | { 95 | $this->current = null; 96 | if ($this->baseCursor) { 97 | $this->baseCursor = null; 98 | $this->iterator = null; 99 | } 100 | } 101 | 102 | public function execute(): void 103 | { 104 | $this->open(); 105 | $this->close(); 106 | } 107 | 108 | public function open(): void 109 | { 110 | if ($this->iterator) { 111 | return; 112 | } 113 | try { 114 | $this->baseCursor = $this->connection 115 | ->getBaseConnection() 116 | ->prepare($this->query); 117 | $this->baseCursor->setFetchMode(\PDO::FETCH_ASSOC); 118 | $this->baseCursor->execute($this->params); 119 | $this->iterator = new IteratorIterator($this->baseCursor); 120 | $this->iterator->rewind(); 121 | $this->current = $this->fetch(); 122 | } catch (\Exception $e) { 123 | throw CursorException::forExecutionFailure($this, $e->getMessage()); 124 | } 125 | } 126 | 127 | public function toArray(): array 128 | { 129 | return iterator_to_array($this); 130 | } 131 | 132 | private function fetch() 133 | { 134 | $fetched = $this->iterator->current(); 135 | if ($this->hydrator && $fetched !== null) { 136 | $fetched = $this->hydrator->hydrate($fetched); 137 | } 138 | 139 | return $fetched; 140 | } 141 | 142 | public function __destruct() 143 | { 144 | $this->close(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Driver/Pdo/Id.php: -------------------------------------------------------------------------------- 1 | entityManager->has($this->getEntityClass(), $id)) { 14 | return $this->entityManager->get($this->getEntityClass(), $id); 15 | } 16 | 17 | $cursor = $this->buildSelectQuery($id); 18 | $cursor->hydrateWith($this->hydrator); 19 | $entity = $cursor->current(); 20 | $cursor->close(); 21 | 22 | if (!$entity instanceof Storable) { 23 | throw RepositoryException::forNotFound($id); 24 | } 25 | 26 | return $entity; 27 | } 28 | 29 | public function create(Storable $entity): Storable 30 | { 31 | // Execute id auto-generation. 32 | $entity->getId(); 33 | $cursor = $this->buildCreateQuery($entity); 34 | $cursor->execute(); 35 | 36 | return $entity; 37 | } 38 | 39 | public function remove(Storable $entity): Storable 40 | { 41 | $cursor = $this->buildDeleteQuery($entity); 42 | $cursor->execute(); 43 | 44 | return $entity; 45 | } 46 | 47 | public function update(Storable $entity): Storable 48 | { 49 | $cursor = $this->buildUpdateQuery($entity); 50 | $cursor->execute(); 51 | 52 | return $entity; 53 | } 54 | 55 | protected function query($query, array $parameters = []): Cursor 56 | { 57 | $cursor = $this->connection->createCursor($query, $parameters); 58 | $cursor->hydrateWith($this->hydrator); 59 | return $cursor; 60 | } 61 | 62 | protected function buildSelectQuery($id): Cursor 63 | { 64 | $query = sprintf( 65 | 'SELECT *FROM %s WHERE %s = :_id', 66 | $this->metaData->getSource(), 67 | $this->metaData->getIdentifier()->getFieldName() 68 | ); 69 | 70 | return $this->connection->createCursor($query, ['_id' => $id]); 71 | } 72 | 73 | protected function buildDeleteQuery(Storable $entity): Cursor 74 | { 75 | $query = sprintf( 76 | 'DELETE FROM %s WHERE %s = :_id', 77 | $this->metaData->getSource(), 78 | $this->metaData->getIdentifier()->getFieldName() 79 | ); 80 | return $this->connection->createCursor($query, ['_id' => $entity->getId()]); 81 | } 82 | 83 | protected function buildCreateQuery(Storable $entity): Cursor 84 | { 85 | $data = $this->hydrator->extract($entity); 86 | $fields = array_keys($data); 87 | $binds = []; 88 | $columns = []; 89 | foreach ($fields as $columnName) { 90 | $columns[] = "\"${columnName}\""; 91 | $binds[] = ":${columnName}"; 92 | } 93 | $sql = sprintf( 94 | 'INSERT INTO %s (%s) VALUES(%s)', 95 | $this->metaData->getSource(), 96 | implode(',', $columns), 97 | implode(',', $binds) 98 | ); 99 | return $this->connection->createCursor($sql, $data); 100 | } 101 | 102 | protected function buildUpdateQuery(Storable $entity): Cursor 103 | { 104 | $data = $this->hydrator->extract($entity); 105 | $fields = array_keys($data); 106 | $columns = []; 107 | foreach ($fields as $columnName) { 108 | $columns[] = "\"${columnName}\" = :${columnName}"; 109 | } 110 | $sql = sprintf( 111 | 'UPDATE %s SET %s WHERE %s = :_id', 112 | $this->metaData->getSource(), 113 | implode(', ', $columns), 114 | $this->metaData->getIdentifier()->getFieldName() 115 | ); 116 | return $this->connection->createCursor($sql, array_merge($data, ['_id' => $entity->getId()])); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Enum.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 17 | $this->metaData = $entityManager->getMetaData($this->getEntityClass()); 18 | } 19 | 20 | public function saveMemory(bool $save = true): void 21 | { 22 | $this->saveMemory = $save; 23 | } 24 | 25 | public function getEntityManager(): EntityManager 26 | { 27 | return $this->entityManager; 28 | } 29 | 30 | public function getMetaData(): EntityMetaData 31 | { 32 | return $this->metaData; 33 | } 34 | 35 | abstract public static function getEntityClass(): string; 36 | } 37 | -------------------------------------------------------------------------------- /src/Hydration/HydratorAutoGenerate.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 24 | $this->autoGenerate = $autoGenerate; 25 | } 26 | 27 | public function get(string $entityClass): ObjectHydrator 28 | { 29 | if (isset($this->hydrators[$entityClass])) { 30 | return $this->hydrators[$entityClass]; 31 | } 32 | 33 | $entityMeta = $this->entityManager->getMetaData($entityClass); 34 | $hydratorClassName = $entityMeta->getHydratorClassName(); 35 | $namespace = $this->entityManager->getHydratorNamespace(); 36 | $hydratorClass = $namespace . '\\' . $hydratorClassName; 37 | 38 | // Fix for already loaded but not initialized hydrator. 39 | if (class_exists($hydratorClass)) { 40 | $objectHydrator = new $hydratorClass($this->entityManager); 41 | if ($entityMeta->definesCustomHydrator()) { 42 | $customHydratorClass = $entityMeta->getCustomHydratorClass(); 43 | $objectHydrator = new $customHydratorClass($objectHydrator); 44 | } 45 | return $this->hydrators[$entityMeta->getClass()] = $objectHydrator; 46 | } 47 | 48 | $fileName = $this->entityManager->getHydratorDir() . DIRECTORY_SEPARATOR . str_replace('\\', '', $hydratorClassName) . '.php'; 49 | switch ($this->autoGenerate) { 50 | case HydratorAutoGenerate::NEVER: 51 | 52 | require_once $fileName; 53 | break; 54 | 55 | case HydratorAutoGenerate::IF_NOT_EXISTS: 56 | 57 | if (!is_readable($fileName)) { 58 | $hydrator = $this->create($entityMeta, true); 59 | $this->writeHydrator($hydrator, $fileName); 60 | } else { 61 | require_once $fileName; 62 | } 63 | break; 64 | 65 | case HydratorAutoGenerate::ALWAYS: 66 | default: 67 | $this->create($entityMeta, true); 68 | break; 69 | } 70 | 71 | $objectHydrator = new $hydratorClass($this->entityManager); 72 | 73 | if ($entityMeta->definesCustomHydrator()) { 74 | $customHydratorClass = $entityMeta->getCustomHydratorClass(); 75 | $objectHydrator = new $customHydratorClass($objectHydrator); 76 | } 77 | 78 | return $this->hydrators[$entityMeta->getClass()] = $objectHydrator; 79 | } 80 | 81 | private function create(EntityMetaData $metaData, bool $load = false): ReflectionApi\RuntimeClass 82 | { 83 | if ($this->entityManager->getHydratorNamespace() !== '') { 84 | $hydratorClass = ReflectionApi::createClass($this->entityManager->getHydratorNamespace() . '\\' . $metaData->getHydratorClassName()); 85 | } else { 86 | $hydratorClass = ReflectionApi::createClass($metaData->getHydratorClassName()); 87 | } 88 | $hydratorClass->extends(GenericHydrator::class); 89 | 90 | $getEntityClassMethod = new ReflectionApi\RuntimeMethod('getEntityClass'); 91 | $getEntityClassMethod->makeStatic(); 92 | $getEntityClassMethod->makeFinal(); 93 | $getEntityClassMethod->setReturnType('string'); 94 | $getEntityClassMethod->setBody( 95 | 'return \\' . $metaData->getClass() . '::class;' 96 | ); 97 | $hydratorClass->addMethod($getEntityClassMethod); 98 | 99 | $hydrateMethod = new ReflectionApi\RuntimeMethod('hydrate'); 100 | $hydrateMethod->addArgument(new ReflectionApi\RuntimeArgument('data', 'array')); 101 | $hydrateMethod->setReturnType($metaData->getClass()); 102 | $hydrateMethod->addLine('$entity = \\' . ReflectionApi::class . '::createInstance(self::getEntityClass());'); 103 | $hydratorClass->addMethod($hydrateMethod); 104 | 105 | $extractMethod = new ReflectionApi\RuntimeMethod('extract'); 106 | $extractMethod->addArgument(new ReflectionApi\RuntimeArgument('entity')); 107 | $extractMethod->setReturnType('array'); 108 | $extractMethod->addLine("\$data = [];"); 109 | $hydratorClass->addMethod($extractMethod); 110 | 111 | foreach ($metaData->getProperties() as $property) { 112 | /** @var MappingStrategy $type */ 113 | $type = $property->getType(); 114 | $attributes = $property->getAttributes(); 115 | 116 | if (method_exists($type, 'getDefaultAttributes')) { 117 | $attributes += $type::getDefaultAttributes(); 118 | } 119 | 120 | $attributes = preg_replace('/\s+/', '', var_export($attributes, true)); 121 | 122 | // Build hydrator for property. 123 | $hydrateMethod->addLine("// Hydrate {$property->getName()}."); 124 | $hydrateMethod->addLine("\$value = \$data['{$property->getFieldName()}'] ?? null;"); 125 | $hydrateMethod->addLine("\\{$type}::hydrate(\$value, ${attributes}, \$this->entityManager);"); 126 | $hydrateMethod->addLine('\\' . ReflectionApi::class . "::writeProperty(\$entity, '{$property->getName()}', \$value);"); 127 | 128 | // Store objects hydrated by reference 129 | $hydrateMethod->addLine("if (\$this->saveMemory === false && \$entity instanceof \Igni\Storage\Storable) {"); 130 | $hydrateMethod->addLine("\t\$this->entityManager->attach(\$entity);"); 131 | $hydrateMethod->addLine("}"); 132 | 133 | // Build extractor for property. 134 | if (!$property->getAttributes()['readonly']) { 135 | $extractMethod->addLine("// Extract {$property->getName()}."); 136 | $extractMethod->addLine('$value = \\' . ReflectionApi::class . "::readProperty(\$entity, '{$property->getName()}');"); 137 | $extractMethod->addLine("\\{$type}::extract(\$value, ${attributes}, \$this->entityManager);"); 138 | $extractMethod->addLine("\$data['{$property->getFieldName()}'] = \$value;"); 139 | } 140 | } 141 | 142 | $hydrateMethod->addLine('return $entity;'); 143 | $extractMethod->addLine('return $data;'); 144 | 145 | if ($load) { 146 | $hydratorClass->load(); 147 | } 148 | 149 | return $hydratorClass; 150 | } 151 | 152 | private function writeHydrator(ReflectionApi\RuntimeClass $hydrator, string $uri): void 153 | { 154 | $temp = fopen($uri, 'w'); 155 | 156 | if ($temp === false || !fwrite($temp, 'id === null) { 15 | $this->id = new Uuid(); 16 | } 17 | 18 | return $this->id; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Id/CompositeId.php: -------------------------------------------------------------------------------- 1 | value = $value; 14 | } 15 | 16 | public function getValue() 17 | { 18 | return $this->value; 19 | } 20 | 21 | public function __toString(): string 22 | { 23 | return implode('.', array_values($this->value)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Id/GenericId.php: -------------------------------------------------------------------------------- 1 | value = $value; 14 | } 15 | 16 | public function getValue() 17 | { 18 | return $this->value; 19 | } 20 | 21 | public function __toString(): string 22 | { 23 | return (string) $this->value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Id/Uuid.php: -------------------------------------------------------------------------------- 1 | long = $value; 18 | return parent::__construct(UuidGenerator::toShort($value)); 19 | } 20 | 21 | $uuid = (string) $value; 22 | $short = (string) $value; 23 | 24 | try { 25 | if (!UuidGenerator::validate($uuid)) { 26 | $uuid = UuidGenerator::fromShort($uuid); 27 | } else { 28 | $short = UuidGenerator::toShort($short); 29 | } 30 | } catch (InvalidArgumentException $exception) { 31 | throw MappingException::forInvalidUuid($value); 32 | } 33 | 34 | if (!UuidGenerator::validate($uuid)) { 35 | throw MappingException::forInvalidUuid($value); 36 | } 37 | 38 | $this->long = $uuid; 39 | parent::__construct($short); 40 | } 41 | 42 | public function getShort(): string 43 | { 44 | return $this->getValue(); 45 | } 46 | 47 | public function getLong(): string 48 | { 49 | return $this->long; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Mapping/Annotation/EmbeddedEntity.php: -------------------------------------------------------------------------------- 1 | type; 32 | } 33 | 34 | public function getAttributes(): array 35 | { 36 | $attributes = get_object_vars($this); 37 | foreach ($attributes as $name => $value) { 38 | $method = 'get' . ucfirst($name); 39 | if (method_exists($this, $method)) { 40 | $attributes[$name] = $this->$method(); 41 | } 42 | } 43 | //unset($attributes['type'], $attributes['value'], $attributes['name']); 44 | 45 | return $attributes; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Mapping/Annotation/Property/Date.php: -------------------------------------------------------------------------------- 1 | format ?? $this->value ?? 'Ymd'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Mapping/Annotation/Property/DecimalNumber.php: -------------------------------------------------------------------------------- 1 | class ?? $this->value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mapping/Annotation/Property/Enum.php: -------------------------------------------------------------------------------- 1 | values ?? (array) $this->value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Mapping/Annotation/Property/FloatNumber.php: -------------------------------------------------------------------------------- 1 | class ?? $this->value ?? Uuid::class; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mapping/Annotation/Property/IntegerNumber.php: -------------------------------------------------------------------------------- 1 | target ?? $this->value; 17 | } 18 | 19 | public function getType(): string 20 | { 21 | return 'reference'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Mapping/Annotation/Property/Text.php: -------------------------------------------------------------------------------- 1 | cursor = $cursor; 26 | } 27 | 28 | public function contains($element): bool 29 | { 30 | foreach ($this as $element) { 31 | if ($element === $element) { 32 | return true; 33 | } 34 | } 35 | 36 | return false; 37 | } 38 | 39 | public function first() 40 | { 41 | $this->pointer = 0; 42 | if ($this->pointer < $this->length) { 43 | return $this->current = $this->items[$this->pointer]; 44 | } 45 | 46 | return $this->current(); 47 | } 48 | 49 | public function last() 50 | { 51 | while ($this->valid()) { 52 | $this->current(); 53 | $this->next(); 54 | } 55 | 56 | return $this->current(); 57 | } 58 | 59 | public function at(int $index) 60 | { 61 | if ($index < $this->length) { 62 | return $this->current = $this->items[$index]; 63 | } 64 | 65 | if ($this->complete) { 66 | throw CollectionException::forOutOfBoundsIndex($index); 67 | } 68 | 69 | $savedPointer = $this->pointer; 70 | while ($this->pointer < $index) { 71 | if (!$this->valid()) { 72 | break; 73 | } 74 | $this->next(); 75 | } 76 | $item = $this->current(); 77 | 78 | if ($this->pointer < $index && $this->complete) { 79 | $offset = $this->pointer; 80 | $this->pointer = $savedPointer; 81 | throw CollectionException::forOutOfBoundsIndex($offset); 82 | } 83 | 84 | $this->pointer = $savedPointer; 85 | 86 | return $item; 87 | } 88 | 89 | public function current() 90 | { 91 | // Empty result-set or out of bounds. 92 | if ($this->cursor === null && 93 | $this->current === null && 94 | ($this->pointer > $this->length || ($this->length === null && $this->complete)) 95 | ) { 96 | throw CollectionException::forOutOfBoundsIndex($this->pointer); 97 | } 98 | 99 | if (null === $this->current) { 100 | if ($this->pointer < $this->length) { 101 | $this->current = $this->items[$this->pointer]; 102 | } else { 103 | $this->items[$this->pointer] = $this->current = $this->cursor->current(); 104 | } 105 | } 106 | 107 | return $this->current; 108 | } 109 | 110 | public function next(): void 111 | { 112 | $this->pointer++; 113 | 114 | if ($this->pointer <= $this->length) { 115 | $this->current = $this->items[$this->pointer]; 116 | } elseif ($this->cursor) { 117 | if ($this->pointer > $this->length) { 118 | $this->length = $this->pointer; 119 | } 120 | $this->cursor->next(); 121 | if ($this->cursor->valid()) { 122 | $this->items[$this->pointer] = $this->current = $this->cursor->current(); 123 | } 124 | } 125 | } 126 | 127 | public function previous(): void 128 | { 129 | if ($this->pointer > 0) { 130 | $this->pointer--; 131 | } 132 | } 133 | 134 | public function key() 135 | { 136 | if (!$this->current) { 137 | $this->current(); 138 | } 139 | return $this->pointer; 140 | } 141 | 142 | public function valid(): bool 143 | { 144 | if ($this->pointer < $this->length) { 145 | return true; 146 | } 147 | 148 | if ($this->cursor) { 149 | $valid = $this->cursor->valid(); 150 | 151 | if (!$valid) { 152 | $this->complete = true; 153 | 154 | $this->cursor->close(); 155 | $this->cursor = null; 156 | } 157 | 158 | return $valid; 159 | } 160 | 161 | return false; 162 | } 163 | 164 | public function rewind(): void 165 | { 166 | $this->pointer = 0; 167 | $this->current = null; 168 | } 169 | 170 | public function count(): int 171 | { 172 | if ($this->complete) { 173 | return $this->length; 174 | } 175 | $this->last(); 176 | 177 | return $this->length; 178 | } 179 | 180 | public function toArray(): array 181 | { 182 | if ($this->complete) { 183 | return $this->items; 184 | } 185 | $this->last(); 186 | 187 | return $this->items; 188 | } 189 | 190 | public function toCollection(): Collection 191 | { 192 | $this->last(); 193 | 194 | return new Collection(new ArrayIterator($this->items)); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Mapping/IdentityMap.php: -------------------------------------------------------------------------------- 1 | class = $class; 85 | $this->reflectionClass = new ReflectionClass($class); 86 | $this->hydratorClassName = '_' . str_replace('\\', '', $class) . 'Hydrator'; 87 | } 88 | 89 | public function makeEmbed(): void 90 | { 91 | $this->source = null; 92 | $this->embed = true; 93 | } 94 | 95 | public function isEmbed(): bool 96 | { 97 | return $this->embed; 98 | } 99 | 100 | public function isStorable(): bool 101 | { 102 | return $this->source !== null && $this->hasIdentifier(); 103 | } 104 | 105 | public function setSource(string $source): void 106 | { 107 | $this->source = $source; 108 | $this->embed = false; 109 | } 110 | 111 | public function getSource(): string 112 | { 113 | return $this->source; 114 | } 115 | 116 | public function setConnection(string $name = 'default'): void 117 | { 118 | $this->connection = $name; 119 | } 120 | 121 | public function getConnection(): string 122 | { 123 | return $this->connection; 124 | } 125 | 126 | public function setCustomHydratorClass(string $className): void 127 | { 128 | if (!class_exists($className)) { 129 | throw new MappingException("Cannot set parent hydrator, class (${className}) does not exists."); 130 | } 131 | $this->customHydrator = $className; 132 | } 133 | 134 | public function definesCustomHydrator(): bool 135 | { 136 | return $this->customHydrator !== null; 137 | } 138 | 139 | public function getCustomHydratorClass(): string 140 | { 141 | return $this->customHydrator; 142 | } 143 | 144 | public function getProperty(string $name): PropertyMetaData 145 | { 146 | if (!isset($this->properties[$name])) { 147 | throw new MappingException("Property ${name} is undefined."); 148 | } 149 | return $this->properties[$name]; 150 | } 151 | 152 | public function addProperty(PropertyMetaData $property): void 153 | { 154 | $this->properties[$property->getName()] = $property; 155 | if ($property->getType() === Id::class) { 156 | $this->identifier = $property; 157 | } 158 | } 159 | 160 | public function hasIdentifier(): bool 161 | { 162 | return $this->identifier !== null; 163 | } 164 | 165 | public function getIdentifier(): PropertyMetaData 166 | { 167 | if (!$this->hasIdentifier()) { 168 | throw new MappingException("Entity {$this->class} defines no identifier."); 169 | } 170 | 171 | return $this->identifier; 172 | } 173 | 174 | public function getHydratorClassName(): string 175 | { 176 | return $this->hydratorClassName; 177 | } 178 | 179 | public function definesProperties(): bool 180 | { 181 | return !empty($this->properties); 182 | } 183 | 184 | /** 185 | * @return PropertyMetaData[] 186 | */ 187 | public function getProperties(): array 188 | { 189 | return $this->properties; 190 | } 191 | 192 | public function getFields(): array 193 | { 194 | return $this->fields; 195 | } 196 | 197 | public function getClass(): string 198 | { 199 | return $this->class; 200 | } 201 | 202 | public function createInstance(...$arguments) 203 | { 204 | if ($arguments) { 205 | return $this->reflectionClass->newInstanceArgs($arguments); 206 | } 207 | 208 | return $this->reflectionClass->newInstanceWithoutConstructor(); 209 | } 210 | 211 | public function __sleep() 212 | { 213 | return [ 214 | 'class', 215 | 'hydratorClassName', 216 | 'properties', 217 | 'source', 218 | 'connection', 219 | 'customHydrator', 220 | ]; 221 | } 222 | 223 | public function __wakeup() 224 | { 225 | $this->reflectionClass = new ReflectionClass($this->class); 226 | 227 | foreach ($this->properties as $property) { 228 | $this->fields[] = $property->getFieldName(); 229 | if ($property->getType() === Id::class) { 230 | $this->identifier = $property; 231 | } 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/Mapping/MetaData/MetaDataFactory.php: -------------------------------------------------------------------------------- 1 | propertyName = $propertyName; 20 | $this->type = $type; 21 | $this->accessor = function () use ($propertyName) { 22 | return $this->$propertyName; 23 | }; 24 | $this->writer = function ($value) use ($propertyName) { 25 | $this->$propertyName = $value; 26 | }; 27 | } 28 | 29 | public function getType(): string 30 | { 31 | return $this->type; 32 | } 33 | 34 | public function getName(): string 35 | { 36 | return $this->propertyName; 37 | } 38 | 39 | public function setAttributes(array $attributes): void 40 | { 41 | $this->attributes = $attributes; 42 | } 43 | 44 | public function getAttributes(): array 45 | { 46 | return $this->attributes; 47 | } 48 | 49 | public function hasAttribute(string $name): bool 50 | { 51 | return isset($this->attributes[$name]); 52 | } 53 | 54 | public function setFieldName(string $name): void 55 | { 56 | $this->fieldName = $name; 57 | } 58 | 59 | public function getFieldName(): string 60 | { 61 | return $this->fieldName ?? $this->propertyName; 62 | } 63 | 64 | public function setValue($object, $value): void 65 | { 66 | Closure::bind($this->writer, $object, $object)($value); 67 | } 68 | 69 | public function getValue($object) 70 | { 71 | return Closure::bind($this->accessor, $object, $object)(); 72 | } 73 | 74 | public function __sleep() 75 | { 76 | return [ 77 | 'attributes', 78 | 'propertyName', 79 | 'fieldName', 80 | 'type', 81 | ]; 82 | } 83 | 84 | public function __wakeup() 85 | { 86 | $propertyName = $this->propertyName; 87 | 88 | $this->accessor = function () use ($propertyName) { 89 | return $this->$propertyName; 90 | }; 91 | $this->writer = function ($value) use ($propertyName) { 92 | $this->$propertyName = $value; 93 | }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Mapping/MetaData/Strategy/AnnotationMetaDataFactory.php: -------------------------------------------------------------------------------- 1 | reader = new IndexedReader(new AnnotationReader()); 40 | 41 | if ($cache === null) { 42 | $cache = new ArrayCachePool(); 43 | } 44 | 45 | $this->cache = $cache; 46 | } 47 | 48 | /** 49 | * @param string $entity 50 | * @return EntityMetaData 51 | */ 52 | public function getMetaData(string $entity): EntityMetaData 53 | { 54 | try { 55 | $metaData = new EntityMetaData($entity); 56 | $reflection = ReflectionApi::reflectClass($entity); 57 | } catch (\ReflectionException $e) { 58 | throw MappingException::forInvalidEntityClass($entity); 59 | } 60 | 61 | $this->parseClassAnnotations($reflection, $metaData); 62 | $this->parseProperties($reflection, $metaData); 63 | 64 | return $metaData; 65 | } 66 | 67 | private function parseClassAnnotations(ReflectionClass $reflection, EntityMetaData $metaData): void 68 | { 69 | $classAnnotations = $this->reader->getClassAnnotations($reflection); 70 | 71 | foreach ($classAnnotations as $type => $annotation) { 72 | switch ($type) { 73 | case Entity::class: 74 | $source = $annotation->source ?? $annotation->value; 75 | $metaData->setSource($source); 76 | $metaData->setConnection($annotation->connection); 77 | $this->setCustomHydrator($annotation, $metaData); 78 | break; 79 | case EmbeddedEntity::class: 80 | $metaData->makeEmbed(); 81 | $this->setCustomHydrator($annotation, $metaData); 82 | break; 83 | } 84 | } 85 | } 86 | 87 | private function parseProperties(ReflectionClass $reflection, EntityMetaData $metaData): void 88 | { 89 | foreach ($reflection->getProperties() as $property) { 90 | $annotations = $this->reader->getPropertyAnnotations($property); 91 | foreach ($annotations as $annotation) { 92 | if ($annotation instanceof Property) { 93 | $this->addProperty($property, $annotation, $metaData); 94 | break; 95 | } 96 | } 97 | } 98 | 99 | if (!$metaData->definesProperties()) { 100 | throw MappingException::forEmptyMapping($metaData->getClass()); 101 | } 102 | } 103 | 104 | /** 105 | * @param EmbeddedEntity|Entity|Annotation $annotation 106 | * @param EntityMetaData $metaData 107 | */ 108 | private function setCustomHydrator(Annotation $annotation, EntityMetaData $metaData) 109 | { 110 | if ($annotation->hydrator !== null) { 111 | if (!class_exists($annotation->hydrator)) { 112 | throw new MappingException("Cannot use hydrator {$annotation->hydrator} class does not exist."); 113 | } 114 | 115 | /** @noinspection PhpStrictTypeCheckingInspection */ 116 | $metaData->setCustomHydratorClass($annotation->hydrator); 117 | } 118 | } 119 | 120 | private function addProperty(ReflectionProperty $property, Property $annotation, EntityMetaData $metaData): void 121 | { 122 | if (!Type::has($annotation->getType())) { 123 | throw new MappingException("Cannot map property {$property->getDeclaringClass()->getName()}::{$property->getName()} - unknown type {$annotation->getType()}."); 124 | } 125 | 126 | $property = new PropertyMetaData( 127 | $property->getName(), 128 | Type::get($annotation->getType()) 129 | ); 130 | $property->setFieldName($annotation->name ?? $property->getName()); 131 | $property->setAttributes($annotation->getAttributes()); 132 | $metaData->addProperty($property); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Mapping/Strategy/Date.php: -------------------------------------------------------------------------------- 1 | format($attributes['format']); 31 | } else { 32 | $value = null; 33 | } 34 | } 35 | 36 | public static function getDefaultAttributes(): array 37 | { 38 | return [ 39 | 'timezone' => 'UTC', 40 | 'format' => 'Ymd', 41 | 'immutable' => false, 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Mapping/Strategy/DecimalNumber.php: -------------------------------------------------------------------------------- 1 | 2, 24 | 'precision' => 10, 25 | ]; 26 | } 27 | 28 | private static function formatDecimalNumber(string $number, array $attributes): string 29 | { 30 | if ($attributes['scale'] > $attributes['precision']) { 31 | throw MappingException::forInvalidAttributeValue( 32 | 'scale', 33 | $attributes['scale'], 34 | 'Attribute `scale` must be lower than `precision`.' 35 | ); 36 | } 37 | 38 | $parts = explode('.', $number); 39 | $decimals = $attributes['precision'] - $attributes['scale']; 40 | 41 | if (strlen($parts[0]) > $decimals) { 42 | $parts[0] = str_repeat('9', $decimals); 43 | } 44 | 45 | $number = $parts[0] . '.' . ($parts[1] ?? ''); 46 | 47 | return bcadd($number, '0', $attributes['scale']); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Mapping/Strategy/DefaultAttributesProvider.php: -------------------------------------------------------------------------------- 1 | hydrate($attributes['class'], $value); 17 | } else { 18 | $value = null; 19 | } 20 | } else { 21 | $value = null; 22 | } 23 | } 24 | 25 | public static function extract(&$value, array $attributes = [], EntityManager $manager = null): void 26 | { 27 | 28 | if ($value instanceof $attributes['class']) { 29 | $value = $manager->extract($value); 30 | $value = self::serializeValue($value, $attributes['storeAs']); 31 | } else { 32 | $value = null; 33 | } 34 | } 35 | 36 | public static function getDefaultAttributes(): array 37 | { 38 | return [ 39 | 'storeAs' => 'json', 40 | ]; 41 | } 42 | 43 | private static function deserializeValue($value, string $strategy) 44 | { 45 | switch ($strategy) { 46 | case 'json': 47 | $value = json_decode($value, true); 48 | break; 49 | case 'serialized': 50 | $value = unserialize($value); 51 | break; 52 | case 'plain': 53 | break; 54 | default: 55 | throw new HydratorException("Cannot persist embed entity, invalid storeAs attribute (${strategy})"); 56 | } 57 | 58 | return $value; 59 | } 60 | 61 | private static function serializeValue($value, string $strategy) 62 | { 63 | switch ($strategy) { 64 | case 'json': 65 | $value = json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION); 66 | break; 67 | case 'serialized': 68 | $value = serialize($value); 69 | break; 70 | case 'plain': 71 | break; 72 | default: 73 | throw new HydratorException("Cannot hydrate embed entity, invalid storeAs attribute (${strategy})"); 74 | } 75 | 76 | return $value; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Mapping/Strategy/Enum.php: -------------------------------------------------------------------------------- 1 | getValue(); 35 | return; 36 | } 37 | 38 | $values = $attributes['values']; 39 | 40 | if (!is_array($values)) { 41 | throw MappingException::forInvalidAttributeValue('values', $values, 'Is not valid class name or available value list.'); 42 | } 43 | 44 | $value = array_search($value, $attributes['values']); 45 | } 46 | 47 | public static function getDefaultAttributes(): array 48 | { 49 | return [ 50 | 'values' => [], 51 | ]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Mapping/Strategy/FloatNumber.php: -------------------------------------------------------------------------------- 1 | getValue(); 20 | } else { 21 | $value = (string) $value; 22 | } 23 | } 24 | 25 | public static function getDefaultAttributes(): array 26 | { 27 | return [ 28 | 'class' => GenericId::class, 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Mapping/Strategy/IntegerNumber.php: -------------------------------------------------------------------------------- 1 | get($attributes['target'], $value); 16 | } catch (\Exception $e) { 17 | $value = null; 18 | } 19 | } 20 | } 21 | 22 | public static function extract(&$value, array $attributes = [], EntityManager $manager = null): void 23 | { 24 | if ($value instanceof Storable) { 25 | $value = $value->getId() ? $value->getId()->getValue() : null; 26 | } else { 27 | $value = null; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Mapping/Strategy/Text.php: -------------------------------------------------------------------------------- 1 | Strategy\Date::class, 11 | 'decimal' => Strategy\DecimalNumber::class, 12 | 'enum' => Strategy\Enum::class, 13 | 'embed' => Strategy\Embed::class, 14 | 'float' => Strategy\FloatNumber::class, 15 | 'id' => Strategy\Id::class, 16 | 'integer' => Strategy\IntegerNumber::class, 17 | 'int' => Strategy\IntegerNumber::class, 18 | 'text' => Strategy\Text::class, 19 | 'string' => Strategy\Text::class, 20 | 'reference' => Strategy\Reference::class, 21 | ]; 22 | 23 | private function __construct() {} 24 | 25 | public static function register(string $type, string $class): void 26 | { 27 | self::$types[$type] = $class; 28 | } 29 | 30 | public static function has(string $type): bool 31 | { 32 | return isset(self::$types[$type]); 33 | } 34 | 35 | /** 36 | * @param string $type 37 | * 38 | * @return MappingStrategy|string 39 | */ 40 | public static function get(string $type): string 41 | { 42 | return self::$types[$type]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Migration.php: -------------------------------------------------------------------------------- 1 | major = $major; 24 | $this->minor = $minor; 25 | $this->patch = $patch; 26 | } 27 | 28 | public function getMajor(): int 29 | { 30 | return $this->major; 31 | } 32 | 33 | public function getMinor(): int 34 | { 35 | return $this->minor; 36 | } 37 | 38 | public function getPatch(): int 39 | { 40 | return $this->patch; 41 | } 42 | 43 | public function compare(Version $version): int 44 | { 45 | if ($version->major > $this->major) { 46 | return 1; 47 | } else if ($this->major > $version->major) { 48 | return -1; 49 | } else if ($version->minor > $this->minor) { 50 | return 1; 51 | } else if ($this->minor > $version->minor) { 52 | return -1; 53 | } else if ($version->patch > $this->patch) { 54 | return 1; 55 | } else if ($this->patch > $version->patch) { 56 | return -1; 57 | } 58 | 59 | return 0; 60 | } 61 | 62 | public function lowerThan(Version $version): bool 63 | { 64 | return $this->compare($version) === 1; 65 | } 66 | 67 | public function lowerOrEquals(Version $version): bool 68 | { 69 | return $this->compare($version) >= 0; 70 | } 71 | 72 | public function greaterThan(Version $version): bool 73 | { 74 | return $this->compare($version) === -1; 75 | } 76 | 77 | public function greaterOrEquals(Version $version): bool 78 | { 79 | return $this->compare($version) <= 0; 80 | } 81 | 82 | public function equals(Version $version): bool 83 | { 84 | return $this->compare($version) === 0; 85 | } 86 | 87 | public function equalsLiteral(string $version): bool 88 | { 89 | return (string) $this === $version; 90 | } 91 | 92 | public function getNextPatch(): self 93 | { 94 | $instance = clone $this; 95 | $instance->patch++; 96 | 97 | return $instance; 98 | } 99 | 100 | public function getNextMinor(): self 101 | { 102 | $instance = clone $this; 103 | $instance->minor++; 104 | 105 | return $instance; 106 | } 107 | 108 | public function getNextMajor(): self 109 | { 110 | $instance = clone $this; 111 | $instance->major++; 112 | 113 | return $instance; 114 | } 115 | 116 | public static function fromString(string $version): self 117 | { 118 | $version = explode('.', $version); 119 | if (count($version) !== 3) { 120 | throw VersionException::forParseError(implode('.', $version)); 121 | } 122 | 123 | return new self((int) $version[0], (int) $version[1], (int) $version[2]); 124 | } 125 | 126 | public function __toString(): string 127 | { 128 | return "{$this->major}.{$this->minor}.{$this->patch}"; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Migration/VersionSynchronizer.php: -------------------------------------------------------------------------------- 1 | synchronizer = $synchronizer; 32 | } 33 | 34 | public function register(Migration $migration): void 35 | { 36 | $this->migrations[] = $migration; 37 | } 38 | 39 | public function getCurrentVersion(): Version 40 | { 41 | return $this->synchronizer->getVersion(); 42 | } 43 | 44 | public function migrate(Version $targetVersion = null): Version 45 | { 46 | $lastVersion = $this->synchronizer->getVersion(); 47 | if ($targetVersion === null) { 48 | $targetVersion = $this->findNewestVersion(); 49 | } 50 | 51 | if ($targetVersion->greaterThan($lastVersion)) { 52 | foreach ($this->migrations as $migration) { 53 | if ($migration->getVersion()->lowerOrEquals($targetVersion)) { 54 | $migration->up(); 55 | } 56 | } 57 | } else if ($targetVersion->lowerThan($lastVersion)) { 58 | foreach ($this->migrations as $migration) { 59 | if ($migration->getVersion()->greaterThan($targetVersion)) { 60 | $migration->down(); 61 | } 62 | } 63 | } 64 | 65 | $this->synchronizer->setVersion($targetVersion); 66 | 67 | return $targetVersion; 68 | } 69 | 70 | private function findNewestVersion(): Version 71 | { 72 | $version = Version::fromString('0.0.0'); 73 | 74 | foreach ($this->migrations as $migration) { 75 | if ($migration->getVersion()->greaterThan($version)) { 76 | $version = clone $migration->getVersion(); 77 | } 78 | } 79 | 80 | return $version; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Repository.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | private $states = []; 24 | 25 | /** 26 | * Contains grouped by class name entities that should be saved. 27 | * @var SplObjectStorage[] 28 | */ 29 | private $create = []; 30 | 31 | /** 32 | * Contains grouped by class name entities that should be removed. 33 | * @var SplObjectStorage[] 34 | */ 35 | private $remove = []; 36 | 37 | /** 38 | * Contains grouped by class name entities that should be removed. 39 | * @var SplObjectStorage[] 40 | */ 41 | private $update = []; 42 | 43 | /** 44 | * @var EntityManager 45 | */ 46 | private $entityManager; 47 | 48 | public function __construct(EntityManager $manager = null) 49 | { 50 | $this->entityManager = $manager ?? new EntityManager(); 51 | } 52 | 53 | public function getRepository(string $entity): Repository 54 | { 55 | return $this->entityManager->getRepository($entity); 56 | } 57 | 58 | public function hasRepository(string $entity): bool 59 | { 60 | return $this->entityManager->hasRepository($entity); 61 | } 62 | 63 | public function addRepository(Repository ...$repositories): void 64 | { 65 | $this->entityManager->addRepository(...$repositories); 66 | } 67 | 68 | public function getEntityManager(): EntityManager 69 | { 70 | return $this->entityManager; 71 | } 72 | 73 | public function get(string $entity, $id): Storable 74 | { 75 | $entity = $this->entityManager->get($entity, $id); 76 | $this->states[spl_object_hash($entity)] = self::STATE_MANAGED; 77 | 78 | return $entity; 79 | } 80 | 81 | /** 82 | * @param Storable[] ...$entities 83 | */ 84 | public function persist(Storable ...$entities): void 85 | { 86 | foreach ($entities as $entity) { 87 | $this->persistOne($entity); 88 | } 89 | } 90 | 91 | /** 92 | * @param Storable[] ...$entities 93 | */ 94 | public function remove(Storable ...$entities): void 95 | { 96 | foreach ($entities as $entity) { 97 | $this->removeOne($entity); 98 | } 99 | } 100 | 101 | public function commit(): void 102 | { 103 | $this->commitAction(self::ACTION_CREATE); 104 | $this->commitAction(self::ACTION_UPDATE); 105 | $this->commitAction(self::ACTION_REMOVE); 106 | } 107 | 108 | private function commitAction(string $action): void 109 | { 110 | foreach ($this->{$action} as $namespace => $entities) { 111 | foreach ($entities as $entity) { 112 | call_user_func([$this->entityManager->getRepository($namespace), $action], $entity); 113 | } 114 | } 115 | } 116 | 117 | public function rollback(): void 118 | { 119 | $this->create = []; 120 | $this->update = []; 121 | $this->remove = []; 122 | } 123 | 124 | private function persistOne(Storable $entity): void 125 | { 126 | $namespace = get_class($entity); 127 | 128 | switch ($this->getState($entity)) { 129 | case self::STATE_MANAGED: 130 | if (!isset($this->update[$namespace])) { 131 | $this->update[$namespace] = new SplObjectStorage(); 132 | } 133 | $this->update[$namespace]->attach($entity); 134 | break; 135 | case self::STATE_NEW: 136 | if (!isset($this->create[$namespace])) { 137 | $this->create[$namespace] = new SplObjectStorage(); 138 | } 139 | $this->create[$namespace]->attach($entity); 140 | break; 141 | case self::STATE_REMOVED: 142 | case self::STATE_DETACHED: 143 | default: 144 | throw UnitOfWorkException::forPersistingEntityInInvalidState($entity); 145 | } 146 | } 147 | 148 | public function getState(Storable $entity): int 149 | { 150 | $oid = spl_object_hash($entity); 151 | if (isset($this->states[$oid])) { 152 | return $this->states[$oid]; 153 | } 154 | 155 | $namespace = get_class($entity); 156 | 157 | if (isset($this->remove[$namespace]) && $this->remove[$namespace]->contains($entity)) { 158 | return $this->states[$oid] = self::STATE_REMOVED; 159 | } 160 | 161 | if ($this->entityManager->contains($entity)) { 162 | return $this->states[$oid] = self::STATE_MANAGED; 163 | } 164 | 165 | try { 166 | $this->entityManager->getRepository($namespace)->get($entity->getId()); 167 | return $this->states[$oid] = self::STATE_DETACHED; 168 | } catch (\Exception $exception) { 169 | return $this->states[$oid] = self::STATE_NEW; 170 | } 171 | } 172 | 173 | private function removeOne(Storable $entity): void 174 | { 175 | $namespace = get_class($entity); 176 | $oid = spl_object_hash($entity); 177 | 178 | if (isset($this->states[$oid]) && $this->states[$oid] === self::STATE_MANAGED) { 179 | $this->states[$oid] = self::STATE_REMOVED; 180 | } 181 | 182 | if (!isset($this->remove[$namespace])) { 183 | $this->remove[$namespace] = new SplObjectStorage(); 184 | } 185 | 186 | if (isset($this->create[$namespace])) { 187 | $this->create[$namespace]->detach($entity); 188 | } 189 | 190 | if (isset($this->update[$namespace])) { 191 | $this->update[$namespace]->detach($entity); 192 | } 193 | 194 | $this->remove[$namespace]->attach($entity); 195 | } 196 | 197 | public function attach(Storable ...$entities): void 198 | { 199 | foreach ($entities as $entity) { 200 | $this->entityManager->attach($entity); 201 | } 202 | } 203 | 204 | public function contains(Storable $entity): bool 205 | { 206 | $contains = $this->entityManager->contains($entity); 207 | $oid = spl_object_hash($entity); 208 | $this->states[$oid] = self::STATE_MANAGED; 209 | 210 | return $contains; 211 | } 212 | 213 | public function detach(Storable ...$entities): void 214 | { 215 | foreach ($entities as $entity) { 216 | $this->detachOne($entity); 217 | } 218 | } 219 | 220 | private function detachOne(Storable $entity): void 221 | { 222 | $this->states[spl_object_hash($entity)] = self::STATE_DETACHED; 223 | $namespace = get_class($entity); 224 | 225 | if (isset($this->update[$namespace])) { 226 | $this->update[$namespace]->detach($entity); 227 | } 228 | 229 | if (isset($this->create[$namespace])) { 230 | $this->create[$namespace]->detach($entity); 231 | } 232 | 233 | if (isset($this->remove[$namespace])) { 234 | $this->remove[$namespace]->detach($entity); 235 | } 236 | 237 | $this->entityManager->detach($entity); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/UnitOfWork.php: -------------------------------------------------------------------------------- 1 | title = $title; 45 | $this->artist = $artist; 46 | } 47 | 48 | public function setTitle(string $title): void 49 | { 50 | $this->title = $title; 51 | } 52 | 53 | public function getTitle(): string 54 | { 55 | return $this->title; 56 | } 57 | 58 | public function getReleaseDate(): DateTime 59 | { 60 | return $this->releaseDate; 61 | } 62 | 63 | public function getArtist(): ArtistEntity 64 | { 65 | return $this->artist; 66 | } 67 | 68 | public function setTracks($tracks): void 69 | { 70 | $this->tracks = $tracks; 71 | } 72 | 73 | public function getTracks(): iterable 74 | { 75 | return $this->tracks ?? []; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Fixtures/Album/AlbumHydrator.php: -------------------------------------------------------------------------------- 1 | baseHydrator = $baseHydrator; 17 | } 18 | 19 | public function hydrate(array $data): AlbumEntity 20 | { 21 | /** @var AlbumEntity $entity */ 22 | $entity = $this->baseHydrator->hydrate($data); 23 | $this->hydrateTracks($entity); 24 | 25 | return $entity; 26 | } 27 | 28 | public function extract($entity): array 29 | { 30 | return $this->baseHydrator->extract($entity); 31 | } 32 | 33 | private function hydrateTracks(AlbumEntity $entity): void 34 | { 35 | /** @var TrackRepository $repository */ 36 | $repository = $this->baseHydrator->getEntityManager()->getRepository(TrackEntity::class); 37 | $entity->setTracks($repository->findByAlbum($entity)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Fixtures/Album/AlbumRepository.php: -------------------------------------------------------------------------------- 1 | query('SELECT * FROM albums'); 14 | } 15 | 16 | public function findByArtist(ArtistEntity $artist): LazyCollection 17 | { 18 | $query = "SELECT * FROM albums WHERE ArtistId = :id"; 19 | 20 | return new LazyCollection($this->query($query, ['id' => $artist->getId()->getValue()])); 21 | } 22 | 23 | public static function getEntityClass(): string 24 | { 25 | return AlbumEntity::class; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Fixtures/Artist/ArtistEntity.php: -------------------------------------------------------------------------------- 1 | name = $name; 34 | } 35 | 36 | public function getName(): string 37 | { 38 | return $this->name; 39 | } 40 | 41 | public function changeName(string $name): void 42 | { 43 | $this->name = $name; 44 | } 45 | 46 | public function getAlbums(): LazyCollection 47 | { 48 | return $this->albums; 49 | } 50 | 51 | public function setAlbums(LazyCollection $albums): void 52 | { 53 | $this->albums = $albums; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Fixtures/Artist/ArtistHydrator.php: -------------------------------------------------------------------------------- 1 | baseHydrator = $hydrator; 16 | } 17 | 18 | public function hydrate(array $data): ArtistEntity 19 | { 20 | $entity = $this->baseHydrator->hydrate($data); 21 | $this->hydrateAlbums($entity); 22 | 23 | return $entity; 24 | } 25 | 26 | public function extract($entity): array 27 | { 28 | return $this->baseHydrator->extract($entity); 29 | } 30 | 31 | private function hydrateAlbums(ArtistEntity $artistEntity) 32 | { 33 | $artistEntity->setAlbums( 34 | $this->baseHydrator->getEntityManager() 35 | ->getRepository(AlbumEntity::class) 36 | ->findByArtist($artistEntity) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Fixtures/Artist/ArtistRepository.php: -------------------------------------------------------------------------------- 1 | query("SELECT 12 | artists.* FROM artists 13 | JOIN albums ON albums.ArtistId = artists.ArtistId 14 | JOIN tracks ON tracks.AlbumId = albums.AlbumId 15 | WHERE tracks.TrackId = :id 16 | ", [ 17 | 'id' => $id 18 | ]); 19 | 20 | return $cursor->current(); 21 | } 22 | 23 | public static function getEntityClass(): string 24 | { 25 | return ArtistEntity::class; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Fixtures/Genre/GenreEntity.php: -------------------------------------------------------------------------------- 1 | name = $name; 34 | $this->id = $id; 35 | } 36 | 37 | public function getName(): string 38 | { 39 | return $this->name; 40 | } 41 | 42 | public function getTracks(): LazyCollection 43 | { 44 | return $this->tracks; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Fixtures/Genre/GenreRepository.php: -------------------------------------------------------------------------------- 1 | rating = $rating; 25 | } 26 | 27 | public function setTracks(LazyCollection $tracks): void 28 | { 29 | $this->tracks = $tracks; 30 | } 31 | 32 | public function getTracks() 33 | { 34 | return $this->tracks; 35 | } 36 | 37 | public function addTrack(TrackEntity $track): void 38 | { 39 | $this->tracks[] = $track; 40 | } 41 | 42 | public function getRating(): float 43 | { 44 | return $this->rating; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Fixtures/Playlist/PlaylistDetailsHydrator.php: -------------------------------------------------------------------------------- 1 | baseHydrator = $baseHydrator; 16 | } 17 | 18 | public function hydrate(array $data) 19 | { 20 | $entity = $this->baseHydrator->hydrate($data); 21 | $this->hydrateTracks($entity, $data['songs']); 22 | 23 | return $entity; 24 | } 25 | 26 | public function extract($entity): array 27 | { 28 | $data = $this->baseHydrator->extract($entity); 29 | $data['songs'] = $this->extractTracks($entity); 30 | 31 | return $data; 32 | } 33 | 34 | 35 | private function hydrateTracks(PlaylistDetails $entity, array $songs) 36 | { 37 | $tracks = $this->baseHydrator->getEntityManager()->getRepository(TrackEntity::class) 38 | ->getMultiple($songs); 39 | 40 | $entity->setTracks($tracks); 41 | } 42 | 43 | private function extractTracks(PlaylistDetails $entity): array 44 | { 45 | $tracks = []; 46 | /** @var TrackEntity $track */ 47 | foreach ($entity->getTracks() as $track) { 48 | $tracks[] = $track->getId()->getValue(); 49 | } 50 | 51 | return $tracks; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Fixtures/Playlist/PlaylistEntity.php: -------------------------------------------------------------------------------- 1 | name = $name; 37 | $this->details = new PlaylistDetails(); 38 | } 39 | 40 | public function getName(): string 41 | { 42 | return $this->name; 43 | } 44 | 45 | public function rename(string $name) 46 | { 47 | $this->name = $name; 48 | } 49 | 50 | public function addTrack(TrackEntity $track): void 51 | { 52 | $this->details->addTrack($track); 53 | } 54 | 55 | public function getRating(): float 56 | { 57 | return $this->details->getRating(); 58 | } 59 | 60 | public function getTracks() 61 | { 62 | return $this->details->getTracks(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Fixtures/Playlist/PlaylistRepository.php: -------------------------------------------------------------------------------- 1 | album = $album; 80 | $this->name = $name; 81 | $this->composer = $composer; 82 | $this->mediaType = self::MEDIA_TYPE_AUDIO_MPEG; 83 | $this->unitPrice = 0; 84 | $this->length = 0; 85 | $this->size = 0; 86 | $this->artist = $album->getArtist(); 87 | } 88 | 89 | public function setPrice(float $price): void 90 | { 91 | $this->unitPrice = $price; 92 | } 93 | 94 | public function getPrice(): float 95 | { 96 | return $this->unitPrice; 97 | } 98 | 99 | public function getComposer(): string 100 | { 101 | return $this->composer; 102 | } 103 | 104 | public function getName(): string 105 | { 106 | return $this->name; 107 | } 108 | 109 | public function getLength(): int 110 | { 111 | return $this->length; 112 | } 113 | 114 | public function getSize(): int 115 | { 116 | return $this->size; 117 | } 118 | 119 | public function setGenre(GenreEntity $genre): void 120 | { 121 | $this->genre = $genre; 122 | } 123 | 124 | public function getGenre(): GenreEntity 125 | { 126 | return $this->genre; 127 | } 128 | 129 | public function getAlbum(): AlbumEntity 130 | { 131 | return $this->album; 132 | } 133 | 134 | public function getArtist(): ArtistEntity 135 | { 136 | return $this->artist; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/Fixtures/Track/TrackRepository.php: -------------------------------------------------------------------------------- 1 | connection->createCursor($query, $ids); 17 | $cursor->hydrateWith($this->hydrator); 18 | 19 | return new LazyCollection($cursor); 20 | } 21 | 22 | public function findByGenreId($id): LazyCollection 23 | { 24 | $query = "SELECT * FROM tracks WHERE GenreId = :id"; 25 | 26 | $cursor = $this->connection->createCursor($query, ['id' => $id]); 27 | $cursor->hydrateWith($this->hydrator); 28 | 29 | return new LazyCollection($cursor); 30 | } 31 | 32 | public function findByAlbum(AlbumEntity $album): LazyCollection 33 | { 34 | $query = "SELECT * FROM albums WHERE AlbumId = :id"; 35 | 36 | $cursor = $this->connection->createCursor($query, ['id' => $album->getId()->getValue()]); 37 | $cursor->hydrateWith($this->hydrator); 38 | 39 | return new LazyCollection($cursor); 40 | } 41 | 42 | public static function getEntityClass(): string 43 | { 44 | return TrackEntity::class; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Fixtures/test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igniphp/storage/dbfa7f0a29a67f02b113efe448346f195180bcf8/tests/Fixtures/test.db -------------------------------------------------------------------------------- /tests/Fixtures/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "playlist": [ 3 | { 4 | "_id": 1, 5 | "name": "Rock Cafe pt. 1", 6 | "details": { 7 | "songs": [ 8 | 1, 9 | 2, 10 | 3, 11 | 4, 12 | 5, 13 | 6, 14 | 7, 15 | 8, 16 | 9, 17 | 10, 18 | 11, 19 | 12 20 | ], 21 | "rating": 4.12 22 | } 23 | }, 24 | { 25 | "_id": 2, 26 | "name": "Rock Cafe pt. 2", 27 | "details": { 28 | "songs": [ 29 | 341, 30 | 342, 31 | 343, 32 | 344, 33 | 345, 34 | 346, 35 | 347, 36 | 348, 37 | 349, 38 | 350, 39 | 351, 40 | 352, 41 | 353, 42 | 354, 43 | 355, 44 | 356, 45 | 357, 46 | 358, 47 | 359, 48 | 419, 49 | 420, 50 | 421, 51 | 422, 52 | 423, 53 | 424, 54 | 425, 55 | 426, 56 | 427, 57 | 428, 58 | 429, 59 | 430, 60 | 431 61 | ], 62 | "rating": 4.87 63 | } 64 | }, 65 | { 66 | "_id": 3, 67 | "name": "Buddy Guy playlist", 68 | "details": { 69 | "songs": [ 70 | 194, 71 | 195, 72 | 196, 73 | 197, 74 | 198, 75 | 199, 76 | 200, 77 | 201, 78 | 202, 79 | 203, 80 | 204 81 | ], 82 | "rating": 2.20 83 | } 84 | }, 85 | { 86 | "_id": 4, 87 | "name": "Really long songs playlist", 88 | "details": { 89 | "songs": [ 90 | 2820, 91 | 3224, 92 | 3226, 93 | 3227, 94 | 3228, 95 | 3229, 96 | 3230, 97 | 3231, 98 | 3232, 99 | 3233, 100 | 3234, 101 | 3235, 102 | 3237, 103 | 3238, 104 | 3239, 105 | 3240, 106 | 3241, 107 | 3242, 108 | 3243, 109 | 3244, 110 | 3245, 111 | 3246, 112 | 3247, 113 | 3248, 114 | 3249 115 | ], 116 | "rating": 3.0 117 | } 118 | }, 119 | { 120 | "_id": 5, 121 | "name": "No composer playlist", 122 | "details": { 123 | "songs": [ 124 | 2, 125 | 63, 126 | 64, 127 | 65, 128 | 66, 129 | 67, 130 | 68, 131 | 69, 132 | 70, 133 | 71, 134 | 72, 135 | 73, 136 | 74, 137 | 75, 138 | 76, 139 | 3389, 140 | 3390, 141 | 3391, 142 | 3392, 143 | 3393, 144 | 3394, 145 | 3395, 146 | 3396, 147 | 3397, 148 | 3398, 149 | 3399, 150 | 3400, 151 | 3401, 152 | 3402, 153 | 131 154 | ], 155 | "rating": 3.50 156 | } 157 | } 158 | ] 159 | } 160 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Driver/MongoDB/ConnectionTest.php: -------------------------------------------------------------------------------- 1 | setupStorage(); 16 | $this->loadRepositories(); 17 | } 18 | 19 | public function testCreate(): void 20 | { 21 | $this->mongoConnection->insert('playlist', [ 22 | 'name' => 'Test playlist', 23 | 'details' => [ 24 | 'rating' => 1.0, 25 | 'songs' => [] 26 | ] 27 | ]); 28 | 29 | $cursor = $this->mongoConnection->find('playlist'); 30 | $cursor->next(); 31 | $cursor->current(); 32 | $cursor->valid(); 33 | 34 | $data = $cursor->toArray(); 35 | 36 | self::assertCount(6, $data); 37 | } 38 | 39 | public function testDelete(): void 40 | { 41 | $this->mongoConnection->remove('playlist', 1); 42 | 43 | $cursor = $this->mongoConnection->find('playlist'); 44 | $data = $cursor->toArray(); 45 | 46 | self::assertCount(4, $data); 47 | } 48 | 49 | public function testFind(): void 50 | { 51 | $cursor = $this->mongoConnection->find('playlist'); 52 | $data = $cursor->toArray(); 53 | 54 | self::assertCount(5, $data); 55 | 56 | $cursor = $this->mongoConnection->find('playlist', ['name' => ['$regex' => 'composer']]); 57 | $data = $cursor->toArray(); 58 | 59 | self::assertCount(1, $data); 60 | 61 | $cursor = $this->mongoConnection->find('playlist', ['details.rating' => ['$gt' => 3]]); 62 | $data = $cursor->toArray(); 63 | 64 | self::assertCount(3, $data); 65 | 66 | } 67 | 68 | public function testUpdate(): void 69 | { 70 | $playlist = $this->mongoConnection->find('playlist', ['_id' => 1])->toArray()[0]; 71 | 72 | $playlist['name'] = 'Rock Kafe pt. I'; 73 | 74 | $this->mongoConnection->update('playlist', $playlist); 75 | $playlistCopy = $this->mongoConnection->find('playlist', ['_id' => 1])->toArray()[0]; 76 | 77 | self::assertSame($playlist, $playlistCopy); 78 | } 79 | 80 | public function tearDown(): void 81 | { 82 | parent::tearDown(); 83 | $this->clearStorage(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Driver/MongoDB/IdTest.php: -------------------------------------------------------------------------------- 1 | getValue()); 16 | self::assertInstanceOf(ObjectId::class, $instance->getValue()); 17 | self::assertSame(24, strlen((string) $instance)); 18 | } 19 | 20 | public function testInstantiateWithPredefinedValue(): void 21 | { 22 | $instance = new Id('5b473ba77c2df709851ba471'); 23 | 24 | self::assertNotEmpty($instance->getValue()); 25 | self::assertInstanceOf(ObjectId::class, $instance->getValue()); 26 | self::assertSame('5b473ba77c2df709851ba471', (string) $instance); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Driver/MongoDB/RepositoryTest.php: -------------------------------------------------------------------------------- 1 | setupStorage(); 19 | $this->loadRepositories(); 20 | } 21 | 22 | public function testGet(): void 23 | { 24 | /** @var PlaylistEntity $playlist */ 25 | $playlist = $this->entityManager->get(PlaylistEntity::class, 1); 26 | 27 | self::assertInstanceOf(PlaylistEntity::class, $playlist); 28 | 29 | $tracks = $playlist->getTracks(); 30 | 31 | self::assertSame(1, $playlist->getId()->getValue()); 32 | self::assertSame(4.12, $playlist->getRating()); 33 | self::assertCount(12, $playlist->getTracks()); 34 | 35 | self::assertInstanceOf(TrackEntity::class, $tracks->at(0)); 36 | 37 | } 38 | 39 | public function testRemove(): void 40 | { 41 | $playlist = $this->entityManager->get(PlaylistEntity::class, 1); 42 | $this->entityManager->remove($playlist); 43 | 44 | $cursor = $this->mongoConnection->find('playlist', ['_id' => 1]); 45 | $dataset = $cursor->toArray(); 46 | 47 | self::assertEmpty($dataset); 48 | } 49 | 50 | public function testCreate(): void 51 | { 52 | 53 | $playlist = new PlaylistEntity('playlistname'); 54 | $playlist->addTrack($this->entityManager->get(TrackEntity::class, 1)); 55 | $playlist->addTrack($this->entityManager->get(TrackEntity::class, 2)); 56 | $playlist->addTrack($this->entityManager->get(TrackEntity::class, 3)); 57 | 58 | $this->entityManager->create($playlist); 59 | 60 | $cursor = new Collection($this->mongoConnection->find('playlist', ['_id' => $playlist->getId()->getValue()])); 61 | self::assertEquals( 62 | [ 63 | 'id' => $playlist->getId()->getValue(), 64 | 'name' => 'playlistname', 65 | 'details' => [ 66 | 'rating' => 0.0, 67 | 'songs' => [1, 2, 3], 68 | ], 69 | ], 70 | $cursor->current() 71 | ); 72 | } 73 | 74 | public function tearDown(): void 75 | { 76 | parent::tearDown(); 77 | $this->clearStorage(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Driver/PDO/CursorTest.php: -------------------------------------------------------------------------------- 1 | setupStorage(); 17 | } 18 | 19 | public function tearDown() 20 | { 21 | parent::tearDown(); 22 | $this->clearStorage(); 23 | } 24 | 25 | public function testCanInstantiate(): void 26 | { 27 | $cursor = $this->createCursorForSql('SELECT * FROM tracks'); 28 | self::assertInstanceOf(Cursor::class, $cursor); 29 | } 30 | 31 | public function testCanIterate(): void 32 | { 33 | $cursor = $this->createCursorForSql('SELECT * FROM tracks'); 34 | $count = 0; 35 | foreach ($cursor as $item) { 36 | $count++; 37 | self::assertArrayHasKey('TrackId', $item); 38 | self::assertArrayHasKey('Name', $item); 39 | self::assertArrayHasKey('AlbumId', $item); 40 | self::assertArrayHasKey('MediaTypeId', $item); 41 | self::assertArrayHasKey('GenreId', $item); 42 | } 43 | 44 | self::assertSame(3503, $count); 45 | } 46 | 47 | private function createCursorForSql(string $sql, array $bind = null) 48 | { 49 | return new Cursor($this->sqliteConnection, $sql, $bind); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Driver/PDO/RepositoryTest.php: -------------------------------------------------------------------------------- 1 | setupStorage(); 18 | $this->loadRepositories(); 19 | } 20 | 21 | public function testGet(): void 22 | { 23 | $albumRepository = $this->entityManager->getRepository(AlbumEntity::class); 24 | 25 | $album = $albumRepository->get(1); 26 | 27 | self::assertInstanceOf(AlbumEntity::class, $album); 28 | 29 | $album2 = $albumRepository->get(1); 30 | 31 | self::assertSame($album, $album2); 32 | 33 | $this->entityManager->clear(); 34 | 35 | $album3 = $albumRepository->get(1); 36 | 37 | self::assertNotSame($album3, $album); 38 | } 39 | 40 | public function testCreate(): void 41 | { 42 | /** @var ArtistEntity $artist */ 43 | $artist = $this->entityManager->get(ArtistEntity::class, 1); 44 | $album = new AlbumEntity('Test Album creation', $artist); 45 | 46 | $this->entityManager->create($album); 47 | 48 | $cursor = $this->sqliteConnection->createCursor( 49 | 'SELECT * FROM albums WHERE AlbumId = :id', 50 | ['id' => $album->getId()->getValue()] 51 | ); 52 | self::assertTrue($cursor->valid()); 53 | $data = $cursor->current(); 54 | self::assertSame( 55 | [ 56 | 'AlbumId' => $album->getId()->getValue(), 57 | 'Title' => $album->getTitle(), 58 | 'ArtistId' => $album->getArtist()->getId()->getValue(), 59 | 'ReleaseDate' => null 60 | ], 61 | $data 62 | ); 63 | } 64 | 65 | public function testRemove(): void 66 | { 67 | $artist = $this->entityManager->get(ArtistEntity::class, 1); 68 | 69 | $this->entityManager->remove($artist); 70 | 71 | $cursor = $this->sqliteConnection->createCursor('SELECT * FROM artists WHERE ArtistId = :id', ['id' => $artist->getId()]); 72 | self::assertFalse($cursor->valid()); 73 | } 74 | 75 | public function testUpdate(): void 76 | { 77 | /** @var ArtistEntity $artist */ 78 | $artist = $this->entityManager->get(ArtistEntity::class, 1); 79 | $artist->changeName('AC (blizzard) DC'); 80 | 81 | $this->entityManager->update($artist); 82 | 83 | $cursor = $this->sqliteConnection->createCursor('SELECT * FROM artists WHERE ArtistId = :id', ['id' => $artist->getId()]); 84 | self::assertTrue($cursor->valid()); 85 | $data = $cursor->current(); 86 | 87 | self::assertSame($artist->getName(), $data['Name']); 88 | } 89 | 90 | public function tearDown(): void 91 | { 92 | parent::tearDown(); 93 | $this->clearStorage(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Hydration/HydratorFactoryTest.php: -------------------------------------------------------------------------------- 1 | provideAlbumMetaData(); 43 | $trackRepository = Mockery::mock(Repository::class); 44 | $trackRepository->shouldReceive('findByAlbum') 45 | ->andReturn(Mockery::mock(LazyCollection::class)); 46 | 47 | $entityManager = Mockery::mock(EntityManager::class); 48 | $entityManager->shouldReceive('attach'); 49 | $entityManager 50 | ->shouldReceive('getHydratorNamespace') 51 | ->andReturn(''); 52 | 53 | $entityManager 54 | ->shouldReceive('getHydratorDir') 55 | ->andReturn(__DIR__ . '/../../../tmp/'); 56 | 57 | $entityManager 58 | ->shouldReceive('get') 59 | ->withArgs([ArtistEntity::class, 12]) 60 | ->andReturn(Mockery::mock(ArtistEntity::class)); 61 | 62 | $entityManager 63 | ->shouldReceive('getMetaData') 64 | ->andReturn($metaData); 65 | 66 | $entityManager 67 | ->shouldReceive('getRepository') 68 | ->withArgs([TrackEntity::class]) 69 | ->andReturn($trackRepository); 70 | 71 | $hydratorFactory = new HydratorFactory( 72 | $entityManager, 73 | HydratorAutoGenerate::ALWAYS 74 | ); 75 | 76 | 77 | $hydratorFactory->get($metaData->getClass()); 78 | 79 | /** @var ObjectHydrator $hydrator */ 80 | $hydrator = $metaData->getHydratorClassName(); 81 | $hydrator = new $hydrator($entityManager); 82 | 83 | self::assertInstanceOf(GenericHydrator::class, $hydrator); 84 | self::assertInstanceOf(EntityMetaData::class, $hydrator->getMetaData()); 85 | 86 | /** @var AlbumEntity $album */ 87 | $album = $hydrator->hydrate($this->provideAlbumData()); 88 | 89 | self::assertInstanceOf(AlbumEntity::class, $album); 90 | self::assertSame('Test Album', $album->getTitle()); 91 | self::assertEquals(new \DateTime('20120101'), $album->getReleaseDate()); 92 | self::assertInstanceOf(ArtistEntity::class, $album->getArtist()); 93 | self::assertSame(1, $album->getId()->getValue()); 94 | } 95 | 96 | public function testHydratorAsSubclass(): void 97 | { 98 | $hydratorDir = __DIR__ . '/../../../tmp'; 99 | $metaData = $this->providePlayListDetailsMetaData(); 100 | 101 | $trackRepository = Mockery::mock(TrackRepository::class); 102 | $trackRepository 103 | ->shouldReceive('getMultiple') 104 | ->andReturn(Mockery::mock(LazyCollection::class)); 105 | 106 | $entityManager = Mockery::mock(EntityManager::class); 107 | $entityManager 108 | ->shouldReceive('getHydratorNamespace') 109 | ->andReturn(''); 110 | $entityManager 111 | ->shouldReceive('getHydratorDir') 112 | ->andReturn($hydratorDir); 113 | 114 | $entityManager 115 | ->shouldReceive('getRepository') 116 | ->andReturn($trackRepository); 117 | $entityManager 118 | ->shouldReceive('getMetaData') 119 | ->andReturn($metaData); 120 | 121 | $hydratorFactory = new HydratorFactory( 122 | $entityManager, 123 | HydratorAutoGenerate::ALWAYS 124 | ); 125 | 126 | $playlistDetailsHydrator = $hydratorFactory->get($metaData->getClass()); 127 | 128 | $playlistDetails = $playlistDetailsHydrator->hydrate([ 129 | 'rating' => '4.2', 130 | 'songs' => [1, 2, 3, 8] 131 | ]); 132 | 133 | self::assertInstanceOf(PlaylistDetails::class, $playlistDetails); 134 | } 135 | 136 | public function testHydratorsFromCustomNamespace(): void 137 | { 138 | $hydratorDir = __DIR__ . '/../../../tmp'; 139 | $metaData = $this->providePlayListDetailsMetaData(); 140 | 141 | $trackRepository = Mockery::mock(TrackRepository::class); 142 | $trackRepository 143 | ->shouldReceive('getMultiple') 144 | ->andReturn(Mockery::mock(LazyCollection::class)); 145 | 146 | $entityManager = Mockery::mock(EntityManager::class); 147 | $entityManager 148 | ->shouldReceive('getHydratorNamespace') 149 | ->andReturn('TestNamespace'); 150 | $entityManager 151 | ->shouldReceive('getHydratorDir') 152 | ->andReturn($hydratorDir); 153 | 154 | $entityManager 155 | ->shouldReceive('getRepository') 156 | ->andReturn($trackRepository); 157 | $entityManager 158 | ->shouldReceive('getMetaData') 159 | ->andReturn($metaData); 160 | 161 | $hydratorFactory = new HydratorFactory( 162 | $entityManager, 163 | HydratorAutoGenerate::ALWAYS 164 | ); 165 | 166 | $playlistDetailsHydrator = $hydratorFactory->get($metaData->getClass()); 167 | 168 | $playlistDetails = $playlistDetailsHydrator->hydrate([ 169 | 'rating' => '4.2', 170 | 'songs' => [1, 2, 3, 8] 171 | ]); 172 | 173 | self::assertInstanceOf(PlaylistDetails::class, $playlistDetails); 174 | } 175 | 176 | private function providePlayListDetailsMetaData(): EntityMetaData 177 | { 178 | $metaData = new EntityMetaData(PlaylistDetails::class); 179 | $metaData->setCustomHydratorClass(PlaylistDetailsHydrator::class); 180 | 181 | $rating = new PropertyMetaData('rating', FloatNumber::class); 182 | $rating->setAttributes(['readonly' => false]); 183 | $metaData->addProperty($rating); 184 | 185 | return $metaData; 186 | } 187 | 188 | private function provideAlbumMetaData(): EntityMetaData 189 | { 190 | $metaData = new EntityMetaData(AlbumEntity::class); 191 | 192 | $id = new PropertyMetaData('id', Id::class); 193 | $id->setFieldName('AlbumId'); 194 | $metaData->addProperty($id); 195 | 196 | $artist = new PropertyMetaData('artist', Reference::class); 197 | $artist->setFieldName('ArtistId'); 198 | $artist->setAttributes(['target' => ArtistEntity::class]); 199 | $metaData->addProperty($artist); 200 | 201 | $title = new PropertyMetaData('title', Text::class); 202 | $title->setFieldName('Title'); 203 | $metaData->addProperty($title); 204 | 205 | $releaseDate = new PropertyMetaData('releaseDate', Date::class); 206 | $releaseDate->setFieldName('ReleaseDate'); 207 | $metaData->addProperty($releaseDate); 208 | 209 | return $metaData; 210 | } 211 | 212 | private function provideAlbumData(): array 213 | { 214 | return [ 215 | 'ReleaseDate' => '20120101', 216 | 'Title' => 'Test Album', 217 | 'ArtistId' => 12, 218 | 'AlbumId' => 1, 219 | ]; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Id/UuidTest.php: -------------------------------------------------------------------------------- 1 | getValue()); 17 | self::assertSame(36, strlen($uuid->getLong())); 18 | self::assertTrue(strlen($uuid->getShort()) >= 21 && strlen($uuid->getShort()) <= 22); 19 | } 20 | 21 | public function testInstantiateWithPredefinedValue(): void 22 | { 23 | $uuid = new Uuid('01f8c1c0-5d38-4a28-aa84-769fdc4259a9'); 24 | 25 | self::assertSame('01f8c1c0-5d38-4a28-aa84-769fdc4259a9', $uuid->getLong()); 26 | self::assertSame('33bdqsTP4y67GBr3sPZNPB', $uuid->getShort()); 27 | 28 | $uuid = new Uuid('33bdqsTP4y67GBr3sPZNPB'); 29 | 30 | self::assertSame('01f8c1c0-5d38-4a28-aa84-769fdc4259a9', $uuid->getLong()); 31 | self::assertSame('33bdqsTP4y67GBr3sPZNPB', $uuid->getShort()); 32 | } 33 | 34 | public function testFailOnInvalidLongUuid(): void 35 | { 36 | $this->expectException(MappingException::class); 37 | new Uuid('invaliduuid'); 38 | } 39 | 40 | public function testFailOnInvalidShortUuid(): void 41 | { 42 | $this->expectException(MappingException::class); 43 | new Uuid(Base58::encode('invaliduuid')); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Mapping/Collection/LazyCollectionTest.php: -------------------------------------------------------------------------------- 1 | setupStorage(); 16 | } 17 | 18 | protected function tearDown(): void 19 | { 20 | $this->clearStorage(); 21 | } 22 | 23 | public function testCanCreate(): void 24 | { 25 | $cursor = $this->createCursorForSql('SELECT *FROM tracks'); 26 | $collection = new LazyCollection($cursor); 27 | 28 | self::assertInstanceOf(LazyCollection::class, $collection); 29 | } 30 | 31 | public function testCount(): void 32 | { 33 | $cursor = $this->createCursorForSql('SELECT *FROM tracks WHERE AlbumId = 1;'); 34 | $collection = new LazyCollection($cursor); 35 | self::assertCount(10, $collection); 36 | } 37 | 38 | public function testToArray(): void 39 | { 40 | $cursor = $this->createCursorForSql('SELECT *FROM tracks WHERE AlbumId = 1;'); 41 | $collection = new LazyCollection($cursor); 42 | 43 | $items = $collection->toArray(); 44 | self::assertCount(10, self::readAttribute($collection, 'items')); 45 | self::assertCount(10, $items); 46 | 47 | foreach ($items as $index => $item) { 48 | self::assertEquals($item, $collection->at($index)); 49 | } 50 | } 51 | 52 | public function testManualIteration(): void 53 | { 54 | $cursor = $this->createCursorForSql('SELECT *FROM tracks WHERE AlbumId = 1;'); 55 | $collection = new LazyCollection($cursor); 56 | self::assertCount(0, self::readAttribute($collection, 'items')); 57 | 58 | $current = $collection->current(); 59 | self::assertEquals(1, $current['TrackId']); 60 | self::assertCount(1, self::readAttribute($collection, 'items')); 61 | 62 | $collection->next(); 63 | $current = $collection->current(); 64 | self::assertEquals(6, $current['TrackId']); 65 | self::assertCount(2, self::readAttribute($collection, 'items')); 66 | 67 | $collection->next(); 68 | $current = $collection->current(); 69 | self::assertEquals(7, $current['TrackId']); 70 | self::assertCount(3, self::readAttribute($collection, 'items')); 71 | 72 | $current = $collection->at(5); 73 | self::assertEquals(10, $current['TrackId']); 74 | self::assertCount(6, self::readAttribute($collection, 'items')); 75 | 76 | $current = $collection->last(); 77 | self::assertEquals(14, $current['TrackId']); 78 | self::assertCount(10, self::readAttribute($collection, 'items')); 79 | self::assertTrue(self::readAttribute($collection, 'complete')); 80 | self::assertNull(self::readAttribute($collection, 'cursor')); 81 | 82 | $current = $collection->first(); 83 | self::assertEquals(1, $current['TrackId']); 84 | self::assertCount(10, self::readAttribute($collection, 'items')); 85 | 86 | $current = $collection->at(2); 87 | self::assertEquals(7, $current['TrackId']); 88 | self::assertCount(10, self::readAttribute($collection, 'items')); 89 | } 90 | 91 | public function testAutomaticIteration(): void 92 | { 93 | $cursor = $this->createCursorForSql('SELECT *FROM tracks WHERE AlbumId = 1;'); 94 | $collection = new LazyCollection($cursor); 95 | $key = 0; 96 | foreach ($collection as $item) { 97 | self::assertArrayHasKey('TrackId', $item); 98 | self::assertCount(9, $item); 99 | self::assertSame($key++, $collection->key()); 100 | } 101 | 102 | self::assertCount(10, self::readAttribute($collection, 'items')); 103 | self::assertCount(10, $collection); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Mapping/MetaData/EntityMetaDataTest.php: -------------------------------------------------------------------------------- 1 | createInstance()); 23 | } 24 | 25 | public function testSerializeAndUnserialize(): void 26 | { 27 | $metaData = new EntityMetaData(ArtistEntity::class); 28 | $metaData->setSource('test'); 29 | $metaData->addProperty(new PropertyMetaData('Name', Text::class)); 30 | 31 | $serialized = serialize($metaData); 32 | /** @var EntityMetaData $unserialized */ 33 | $unserialized = unserialize($serialized); 34 | 35 | self::assertInstanceOf(EntityMetaData::class, $unserialized); 36 | self::assertSame(ArtistEntity::class, $unserialized->getClass()); 37 | self::assertSame('test', $unserialized->getSource()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Mapping/MetaData/PropertyMetaDataTest.php: -------------------------------------------------------------------------------- 1 | setFieldName('Name'); 22 | $property->setAttributes(['length' => 20]); 23 | $serialized = serialize($property); 24 | $unserialized = unserialize($serialized); 25 | 26 | self::assertSame('Name', $unserialized->getFieldName()); 27 | self::assertSame('name', $unserialized->getName()); 28 | self::assertSame(Text::class, $unserialized->getType()); 29 | self::assertEquals(['length' => 20], $unserialized->getAttributes()); 30 | $this->setAndGetName($unserialized); 31 | } 32 | 33 | public function testSetGetValue(): void 34 | { 35 | $property = new PropertyMetaData('name', Text::class); 36 | $this->setAndGetName($property); 37 | } 38 | 39 | private function setAndGetName(PropertyMetaData $property): void 40 | { 41 | $artist = new ArtistEntity('Test Name'); 42 | 43 | self::assertSame('Test Name', $property->getValue($artist)); 44 | $property->setValue($artist, 'Updated Name'); 45 | self::assertSame('Updated Name', $property->getValue($artist)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Mapping/MetaData/Strategy/AnnotationMetaDataFactoryTest.php: -------------------------------------------------------------------------------- 1 | getMetaData(PlaylistEntity::class); 24 | 25 | self::assertCount(3, $metaData->getProperties()); 26 | self::assertSame('playlist', $metaData->getSource()); 27 | self::assertSame(PlaylistEntity::class, $metaData->getClass()); 28 | self::assertFalse($metaData->isEmbed()); 29 | 30 | $properties = $metaData->getProperties(); 31 | self::assertCount(3, $properties); 32 | 33 | self::assertSame(Id::class, $metaData->getProperty('id')->getType()); 34 | self::assertSame(Text::class, $metaData->getProperty('name')->getType()); 35 | self::assertSame(Embed::class, $metaData->getProperty('details')->getType()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Mapping/Strategy/DateTest.php: -------------------------------------------------------------------------------- 1 | 'd-Y-m', 15 | ] + Date::getDefaultAttributes(); 16 | Date::extract($value, $attributes); 17 | 18 | self::assertSame('01-2017-01', $value); 19 | } 20 | 21 | public function testExtractNull(): void 22 | { 23 | $value = null; 24 | $attributes = Date::getDefaultAttributes(); 25 | Date::extract($value, $attributes); 26 | 27 | self::assertNull($value); 28 | } 29 | 30 | public function testHydrate(): void 31 | { 32 | $date = '2017-01-01'; 33 | $value = $date; 34 | $attributes = Date::getDefaultAttributes(); 35 | Date::hydrate($value, $attributes); 36 | 37 | self::assertEquals(new \DateTime($date), $value); 38 | } 39 | 40 | public function testHydrateNull(): void 41 | { 42 | $value = null; 43 | $attributes = Date::getDefaultAttributes(); 44 | Date::hydrate($value, $attributes); 45 | 46 | self::assertNull($value); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Mapping/Strategy/DecimalNumberTest.php: -------------------------------------------------------------------------------- 1 | 2, 53 | 'precision' => 4, 54 | ]; 55 | DecimalNumber::hydrate($value, $attributes); 56 | 57 | self::assertSame('99.00', $value); 58 | } 59 | 60 | public function testScaleExcessPrecision(): void 61 | { 62 | $this->expectException(MappingException::class); 63 | $value = '1'; 64 | $attributes = [ 65 | 'scale' => 9, 66 | 'precision' => 4, 67 | ]; 68 | DecimalNumber::hydrate($value, $attributes); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Mapping/Strategy/EmbedTest.php: -------------------------------------------------------------------------------- 1 | 2.0, 18 | ]; 19 | $entityManager = Mockery::mock(EntityManager::class); 20 | $entityManager 21 | ->shouldReceive('extract') 22 | ->withArgs([$playlistDetails]) 23 | ->andReturn($extracted); 24 | $value = $playlistDetails; 25 | 26 | $attributes = [ 27 | 'class' => PlaylistDetails::class, 28 | ] + Embed::getDefaultAttributes(); 29 | Embed::extract($value, $attributes, $entityManager); 30 | 31 | self::assertSame(json_encode($extracted, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION), $value); 32 | } 33 | 34 | public function testExtractNull(): void 35 | { 36 | $value = null; 37 | 38 | $attributes = [ 39 | 'class' => PlaylistDetails::class, 40 | ] + Embed::getDefaultAttributes(); 41 | Embed::extract($value, $attributes); 42 | 43 | self::assertNull($value); 44 | } 45 | 46 | public function testHydrate(): void 47 | { 48 | $playlistDetails = new PlaylistDetails(2.0); 49 | $extracted = [ 50 | 'rating' => 2.0, 51 | ]; 52 | $serialized = json_encode($extracted, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION); 53 | $entityManager = Mockery::mock(EntityManager::class); 54 | $entityManager 55 | ->shouldReceive('hydrate') 56 | ->withArgs([PlaylistDetails::class, $extracted]) 57 | ->andReturn($playlistDetails); 58 | $value = $serialized; 59 | 60 | $attributes = [ 61 | 'class' => PlaylistDetails::class, 62 | ] + Embed::getDefaultAttributes(); 63 | Embed::hydrate($value, $attributes, $entityManager); 64 | 65 | self::assertSame($playlistDetails, $value); 66 | } 67 | 68 | public function testHydrateNull(): void 69 | { 70 | $value = null; 71 | 72 | $attributes = [ 73 | 'class' => PlaylistDetails::class, 74 | ] + Embed::getDefaultAttributes(); 75 | Embed::hydrate($value, $attributes); 76 | 77 | self::assertNull($value); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Mapping/Strategy/EnumTest.php: -------------------------------------------------------------------------------- 1 | expectException(MappingException::class); 14 | Enum::extract($value, ['values' => 1]); 15 | } 16 | 17 | public function testHydrationFailOnNonExistentClass(): void 18 | { 19 | $this->expectException(MappingException::class); 20 | $value = 'a'; 21 | Enum::hydrate($value, ['values' => 'SomeUnkonwnClass']); 22 | } 23 | 24 | public function testHydrationFailOnEmptyArray(): void 25 | { 26 | $this->expectException(MappingException::class); 27 | $value = 'a'; 28 | Enum::hydrate($value, ['values' => []]); 29 | } 30 | 31 | /** 32 | * @param array $attributes 33 | * @param $value 34 | * @param $expected 35 | * @dataProvider provideValidDataForExtraction 36 | */ 37 | public function testExtract(array $attributes, $value, $expected): void 38 | { 39 | Enum::extract($value, $attributes); 40 | self::assertSame($expected, $value); 41 | } 42 | 43 | /** 44 | * @param array $attributes 45 | * @param $value 46 | * @param $expected 47 | * @dataProvider provideValidDataForHydration 48 | */ 49 | public function testHydrate(array $attributes, $value, $expected): void 50 | { 51 | Enum::hydrate($value, $attributes); 52 | self::assertEquals($expected, $value); 53 | } 54 | 55 | public function provideValidDataForHydration(): array 56 | { 57 | $list = ['values' => ['a', 'b', 'c']]; 58 | $class = ['values' => AbcEnum::class]; 59 | 60 | return [ 61 | [ 62 | [], 63 | null, 64 | null 65 | ], 66 | [ 67 | $list, 68 | 0, 69 | 'a', 70 | ], 71 | [ 72 | $class, 73 | 0, 74 | new AbcEnum(0), 75 | ], 76 | [ 77 | $list, 78 | 2, 79 | 'c', 80 | ], 81 | [ 82 | $class, 83 | 2, 84 | new AbcEnum(2), 85 | ], 86 | ]; 87 | } 88 | 89 | public function provideValidDataForExtraction(): array 90 | { 91 | $list = ['values' => ['a', 'b', 'c']]; 92 | $class = ['values' => AbcEnum::class]; 93 | 94 | return [ 95 | [ 96 | $list, 97 | 'a', 98 | 0, 99 | ], 100 | [ 101 | $class, 102 | new AbcEnum(0), 103 | 'a', 104 | ], 105 | [ 106 | $list, 107 | 'c', 108 | 2, 109 | ], 110 | [ 111 | $class, 112 | new AbcEnum(2), 113 | 'c', 114 | ], 115 | ]; 116 | } 117 | } 118 | 119 | class AbcEnum implements \Igni\Storage\Enum 120 | { 121 | private const VALUES = ['a', 'b', 'c']; 122 | private $value; 123 | 124 | public function __construct($value) 125 | { 126 | $this->value = self::VALUES[$value]; 127 | } 128 | 129 | public function getValue() 130 | { 131 | return $this->value; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/Functional/Storage/Mapping/Strategy/FloatNumberTest.php: -------------------------------------------------------------------------------- 1 | createMock(VersionSynchronizer::class)) 19 | ); 20 | } 21 | 22 | /** 23 | * @param Migration[] $migrations 24 | * @dataProvider provideMigrations 25 | */ 26 | public function testRegister(array $migrations): void 27 | { 28 | $migrationManager = new MigrationManager($this->createMock(VersionSynchronizer::class)); 29 | foreach ($migrations as $migration) { 30 | $migrationManager->register($migration); 31 | } 32 | 33 | self::assertCount(8, self::readAttribute($migrationManager, 'migrations')); 34 | } 35 | 36 | public function testGetCurrentVersion(): void 37 | { 38 | $versionSynchronizerMock = $this->createMock(VersionSynchronizer::class); 39 | $versionSynchronizerMock->method('getVersion')->willReturn(Version::fromString('0.0.0')); 40 | $migrationManager = new MigrationManager($versionSynchronizerMock); 41 | 42 | self::assertTrue($migrationManager->getCurrentVersion()->equals(Version::fromString('0.0.0'))); 43 | } 44 | 45 | /** 46 | * @param Migration[] $migrations 47 | * @dataProvider provideMigrations 48 | */ 49 | public function testMigrateUp(array $migrations): void 50 | { 51 | $versionSynchronizerMock = $this->createMock(VersionSynchronizer::class); 52 | $versionSynchronizerMock->method('getVersion')->willReturn(Version::fromString('0.0.0')); 53 | $migrationManager = new MigrationManager($versionSynchronizerMock); 54 | foreach ($migrations as $migration) { 55 | $migrationManager->register($migration); 56 | } 57 | 58 | $version = $migrationManager->migrate(); 59 | self::assertTrue($version->equalsLiteral('2.0.0')); 60 | } 61 | 62 | /** 63 | * @param Migration[] $migrations 64 | * @dataProvider provideMigrations 65 | */ 66 | public function testMigrateDown(array $migrations): void 67 | { 68 | $version = Version::fromString('2.0.0'); 69 | $versionSynchronizerMock = $this->createMock(VersionSynchronizer::class); 70 | $versionSynchronizerMock->method('getVersion')->willReturn($version); 71 | $migrationManager = new MigrationManager($versionSynchronizerMock); 72 | foreach ($migrations as $migration) { 73 | $migrationManager->register($migration); 74 | } 75 | $migrationManager->register($migration = new class implements Migration { 76 | public $up = false; 77 | public $down = false; 78 | 79 | public function up(): void 80 | { 81 | $this->up = true; 82 | } 83 | 84 | public function down(): void 85 | { 86 | $this->down = true; 87 | } 88 | 89 | public function getVersion(): Version 90 | { 91 | return Version::fromString('1.9.0'); 92 | } 93 | }); 94 | 95 | $version = $migrationManager->migrate(Version::fromString('1.8.0')); 96 | 97 | self::assertTrue($version->equalsLiteral('1.8.0')); 98 | self::assertTrue($migration->down); 99 | self::assertFalse($migration->up); 100 | } 101 | 102 | public function provideMigrations(): array 103 | { 104 | $migrations = [ 105 | '2.0.0' => $this->createMock(Migration::class), 106 | '1.0.0' => $this->createMock(Migration::class), 107 | '0.1.0' => $this->createMock(Migration::class), 108 | '0.1.2' => $this->createMock(Migration::class), 109 | '1.1.2' => $this->createMock(Migration::class), 110 | '1.2.2' => $this->createMock(Migration::class), 111 | '1.0.1' => $this->createMock(Migration::class), 112 | '0.0.1' => $this->createMock(Migration::class), 113 | ]; 114 | 115 | /** 116 | * @var string $version 117 | * @var MockObject|Migration $migration 118 | */ 119 | foreach ($migrations as $version => $migration) { 120 | $migration 121 | ->method('getVersion') 122 | ->willReturn(Version::fromString($version)); 123 | } 124 | 125 | return [[$migrations]]; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/Functional/Storage/StorageTest.php: -------------------------------------------------------------------------------- 1 | setupStorage(); 20 | $this->loadRepositories(); 21 | } 22 | 23 | public function testCanInstantiate(): void 24 | { 25 | $entityStorage = new Storage(new EntityManager()); 26 | 27 | self::assertInstanceOf(Storage::class, $entityStorage); 28 | } 29 | 30 | public function testGet(): void 31 | { 32 | $artist = $this->unitOfWork->get(ArtistEntity::class, 1); 33 | 34 | self::assertInstanceOf(ArtistEntity::class, $artist); 35 | } 36 | 37 | public function testPersistNewEntity(): void 38 | { 39 | $count = $this->queryArtistsCount(); 40 | $artist = new ArtistEntity("New Test Artist"); 41 | $this->unitOfWork->persist($artist); 42 | 43 | self::assertSame($count, $this->queryArtistsCount()); 44 | } 45 | 46 | public function testPersistAndCommitNewEntity(): void 47 | { 48 | $count = $this->queryArtistsCount(); 49 | $artist = new ArtistEntity("New Test Artist"); 50 | $this->unitOfWork->persist($artist, $artist); 51 | 52 | self::assertSame($count, $this->queryArtistsCount()); 53 | $this->unitOfWork->commit(); 54 | self::assertSame($count + 1, $this->queryArtistsCount()); 55 | } 56 | 57 | public function testPersistModifiedEntity(): void 58 | { 59 | /** @var ArtistEntity $artist */ 60 | $artist = $this->unitOfWork->get(ArtistEntity::class, 1); 61 | $artist->changeName('John Bohn Ohn'); 62 | $this->unitOfWork->persist($artist); 63 | 64 | $this->unitOfWork->commit(); 65 | 66 | $artist = $this->executeSql( 67 | 'SELECT artists.Name FROM artists WHERE ArtistId = :id', 68 | ['id' => 1] 69 | )->fetch(); 70 | 71 | self::assertSame('John Bohn Ohn', $artist['Name']); 72 | } 73 | 74 | public function testRemoveEntity(): void 75 | { 76 | /** @var ArtistEntity $artist */ 77 | $artist = $this->unitOfWork->get(ArtistEntity::class, 1); 78 | $count = $this->queryArtistsCount(); 79 | $this->unitOfWork->remove($artist); 80 | self::assertSame($count, $this->queryArtistsCount()); 81 | $this->unitOfWork->commit(); 82 | self::assertSame($count - 1, $this->queryArtistsCount()); 83 | } 84 | 85 | public function testRollback(): void 86 | { 87 | $count = $this->queryArtistsCount(); 88 | /** @var ArtistEntity $artist */ 89 | $artist = $this->unitOfWork->get(ArtistEntity::class, 1); 90 | 91 | $artist->changeName('John Bohn Ohn'); 92 | $this->unitOfWork->persist($artist); 93 | 94 | $artist2 = $this->unitOfWork->get(ArtistEntity::class, 2); 95 | $this->unitOfWork->remove($artist2); 96 | 97 | $this->unitOfWork->rollback(); 98 | $this->unitOfWork->persist(); 99 | 100 | $artistData = $this->executeSql( 101 | 'SELECT artists.Name FROM artists WHERE ArtistId = :id', 102 | ['id' => 1] 103 | )->fetch(); 104 | self::assertNotSame('John Bohn Ohn', $artistData['Name']); 105 | self::assertSame($count, $this->queryArtistsCount()); 106 | 107 | } 108 | 109 | public function testAttachThanDetach(): void 110 | { 111 | $artist = new ArtistEntity("New Test Artist"); 112 | $this->unitOfWork->attach($artist); 113 | 114 | self::assertTrue($this->entityManager->contains($artist)); 115 | self::assertTrue($this->unitOfWork->contains($artist)); 116 | 117 | $this->unitOfWork->detach($artist); 118 | 119 | self::assertFalse($this->entityManager->contains($artist)); 120 | self::assertFalse($this->unitOfWork->contains($artist)); 121 | } 122 | 123 | public function testGetEntityManager(): void 124 | { 125 | self::assertInstanceOf(EntityManager::class, $this->unitOfWork->getEntityManager()); 126 | 127 | $storage = new Storage(); 128 | self::assertInstanceOf(EntityManager::class, $storage->getEntityManager()); 129 | } 130 | 131 | public function testWorkingWithRepositories() 132 | { 133 | $storage = new Storage(); 134 | $playlistRepository = $this->createPlaylistRepository(); 135 | $storage->addRepository($playlistRepository); 136 | self::assertTrue($storage->hasRepository(PlaylistEntity::class)); 137 | self::assertSame($playlistRepository, $storage->getRepository(PlaylistEntity::class)); 138 | } 139 | 140 | public function testPersistOnDetachedEntity(): void 141 | { 142 | $this->expectException(StorageException::class); 143 | $storage = new Storage(); 144 | $artist = new ArtistEntity("New Test Artist"); 145 | $storage->remove($artist); 146 | $storage->persist($artist); 147 | } 148 | 149 | private function queryArtistsCount(): int 150 | { 151 | return (int) $this->executeSql('SELECT count(ArtistId) as count FROM artists')->fetchColumn(0); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /tests/Functional/Storage/StorageTrait.php: -------------------------------------------------------------------------------- 1 | sqliteDbPath = tempnam(sys_get_temp_dir(), 'igni-test'); 45 | copy($dbDir . '/test.db', $this->sqliteDbPath); 46 | 47 | $this->sqliteConnection = new SqliteConnection('sqlite:' . $this->sqliteDbPath); 48 | $this->mongoConnection = new MongoDBConnection('localhost', new MongoDBOptions('test', 'travis', 'test')); 49 | ConnectionManager::registerDefault($this->sqliteConnection); 50 | ConnectionManager::register('sqlite', $this->sqliteConnection); 51 | ConnectionManager::register('mongo', $this->mongoConnection); 52 | 53 | $this->entityManager = new EntityManager($tmpDir); 54 | $this->unitOfWork = new Storage($this->entityManager); 55 | 56 | $mongoData = json_decode(file_get_contents($dbDir . '/test.json'), true); 57 | foreach ($mongoData as $collection => $data) { 58 | try { 59 | $this->mongoConnection->dropCollection($collection); 60 | } catch (\Exception $e) { 61 | // Ignore missing collection error. 62 | } 63 | $this->mongoConnection->insert($collection, ...$data); 64 | } 65 | } 66 | 67 | private function executeSql(string $query, array $bind = null): \PDOStatement 68 | { 69 | $statement = $this->sqliteConnection->getBaseConnection()->prepare($query); 70 | $statement->execute($bind); 71 | 72 | return $statement; 73 | } 74 | 75 | private function clearStorage(): void 76 | { 77 | unlink($this->sqliteDbPath); 78 | } 79 | 80 | private function createCursorForSql(string $sql, array $bind = null): Cursor 81 | { 82 | return $this->sqliteConnection->createCursor($sql, $bind); 83 | } 84 | 85 | private function loadRepositories(): void 86 | { 87 | $this->entityManager->addRepository( 88 | $this->createArtistRepository(), 89 | $this->createAlbumRepository(), 90 | $this->createTrackRepository(), 91 | $this->createGenreRepository(), 92 | $this->createPlaylistRepository() 93 | ); 94 | } 95 | 96 | private function createArtistRepository(): ArtistRepository 97 | { 98 | return new ArtistRepository($this->entityManager); 99 | } 100 | 101 | private function createAlbumRepository(): AlbumRepository 102 | { 103 | return new AlbumRepository($this->entityManager); 104 | } 105 | 106 | private function createTrackRepository(): TrackRepository 107 | { 108 | return new TrackRepository($this->entityManager); 109 | } 110 | 111 | private function createGenreRepository(): GenreRepository 112 | { 113 | return new GenreRepository($this->entityManager); 114 | } 115 | 116 | private function createPlaylistRepository(): PlaylistRepository 117 | { 118 | return new PlaylistRepository($this->entityManager); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/Unit/Storage/Migration/VersionTest.php: -------------------------------------------------------------------------------- 1 | getMajor()); 20 | self::assertSame(2, $version->getMinor()); 21 | self::assertSame(3, $version->getPatch()); 22 | } 23 | 24 | public function testGetMajor(): void 25 | { 26 | $version = new Version(1, 0,0 ); 27 | self::assertSame(1, $version->getMajor()); 28 | } 29 | 30 | public function testGetMinor(): void 31 | { 32 | $version = new Version(0, 1,0 ); 33 | self::assertSame(1, $version->getMinor()); 34 | } 35 | 36 | public function testGetPatch(): void 37 | { 38 | $version = new Version(0, 0,1 ); 39 | self::assertSame(1, $version->getPatch()); 40 | } 41 | 42 | public function testGetNextMajor(): void 43 | { 44 | $version = new Version(0, 0,0 ); 45 | $next = $version->getNextMajor(); 46 | self::assertNotSame($next, $version); 47 | self::assertSame('1.0.0', (string) $next); 48 | } 49 | 50 | public function testGetNextMinor(): void 51 | { 52 | $version = new Version(0, 0,0 ); 53 | $next = $version->getNextMinor(); 54 | self::assertNotSame($next, $version); 55 | self::assertSame('0.1.0', (string) $next); 56 | } 57 | 58 | public function testGetNextPatch(): void 59 | { 60 | $version = new Version(0, 0,0 ); 61 | $next = $version->getNextPatch(); 62 | self::assertNotSame($next, $version); 63 | self::assertSame('0.0.1', (string) $next); 64 | } 65 | 66 | public function testGreaterThan(): void 67 | { 68 | $version = Version::fromString('0.0.0'); 69 | $equalsVersion = Version::fromString('0.0.0'); 70 | $greaterMinor = Version::fromString('0.1.0'); 71 | 72 | self::assertFalse($version->greaterThan($equalsVersion)); 73 | self::assertFalse($version->greaterThan($greaterMinor)); 74 | self::assertTrue($greaterMinor->greaterThan($version)); 75 | } 76 | 77 | public function testGreaterOrEquals(): void 78 | { 79 | $version = Version::fromString('0.0.0'); 80 | $equalsVersion = Version::fromString('0.0.0'); 81 | $greaterMinor = Version::fromString('0.1.0'); 82 | 83 | self::assertTrue($version->greaterOrEquals($equalsVersion)); 84 | self::assertTrue($greaterMinor->greaterOrEquals($version)); 85 | self::assertFalse($version->greaterOrEquals($greaterMinor)); 86 | } 87 | 88 | public function testLowerThan(): void 89 | { 90 | $version = Version::fromString('0.0.0'); 91 | $equalsVersion = Version::fromString('0.0.0'); 92 | $greaterMinor = Version::fromString('0.1.0'); 93 | 94 | self::assertFalse($version->lowerThan($equalsVersion)); 95 | self::assertTrue($version->lowerThan($greaterMinor)); 96 | self::assertFalse($greaterMinor->lowerThan($version)); 97 | } 98 | 99 | public function testLowerOrEquals(): void 100 | { 101 | $version = Version::fromString('0.0.0'); 102 | $equalsVersion = Version::fromString('0.0.0'); 103 | $greaterMinor = Version::fromString('0.1.0'); 104 | 105 | self::assertTrue($version->lowerOrEquals($equalsVersion)); 106 | self::assertTrue($version->lowerOrEquals($greaterMinor)); 107 | self::assertFalse($greaterMinor->lowerOrEquals($version)); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |