diff --git a/.travis.yml b/.travis.yml index b8f464d..4ca6e0b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: php php: - - 7.0 - 7.1 - 7.2 @@ -9,7 +8,7 @@ branches: only: - master - develop - - /^v\d+\.\d+\.\d+$/ + - /^d+\.\d+$/ cache: directories: diff --git a/composer.json b/composer.json index 8047bcb..89e9d92 100644 --- a/composer.json +++ b/composer.json @@ -10,22 +10,32 @@ } ], "require": { + "php": "^7.1", "symfony/property-access": "^3.0|^4.0" }, "require-dev": { "doctrine/orm": "^2.4,>=2.4.5", - "phpunit/phpunit": "^6.3", + "phpunit/phpunit": "^7.0", "friendsofphp/php-cs-fixer": "^2.7", - "phpmd/phpmd": "^2.6" + "phpmd/phpmd": "^2.6", + "phpstan/phpstan": "^0.9.2", + "symfony/var-dumper": "^4.0", + "symfony/yaml": "^4.0" }, "suggest": { "doctrine/orm": "^2.4,>=2.4.5", "webonyx/graphql-php": "^0.0,>=0.9" }, "autoload": { - "psr-4": { "Xsolve\\Associate\\": "lib" } + "psr-4": { + "Xsolve\\Associate\\": "lib", + "Xsolve\\LegacyAssociate\\": "legacyLib" + } }, "autoload-dev": { - "psr-4": { "Xsolve\\AssociateTests\\": "tests" } + "psr-4": { + "Xsolve\\AssociateTests\\": "tests", + "Xsolve\\LegacyAssociateTests\\": "legacyTests" + } } } diff --git a/draft-1.php b/draft-1.php new file mode 100644 index 0000000..41f19e0 --- /dev/null +++ b/draft-1.php @@ -0,0 +1,96 @@ +getTargets($sources, $path); + +$sourcesToTargetsMap = $collector->getSourcesToTargetsMap($sources, $path); +$targets1 = $sourcesToTargetsMap->get([$source1]); +$targets1And2 = $sourcesToTargetsMap->get([$source1, $source2]); + +$targetIds = $collector->getTargetIds($sources, $path); + +$sourcesToTargetIdsMap = $collector->getSourcesToTargetIdsMap($sources, $path); +$targetIds1 = $sourcesToTargetIdsMap->get([$source1]); +$targetIds1And2 = $sourcesToTargetIdsMap->get([$source1, $source2]); + +// Need to specify strategy for final objects - if they should be fully loaded or if proxies are enough. + +$doctrineOrmAssociationPath = $doctrineOrmAssociationPathBuilder + ->associate('bar') + ->aliasAs('bars') + ->loadFull() + ->associate('baz') + ->aliasAs('bazs') + ->create(); + +$doctrineOrmAssociationPath = $doctrineOrmAssociationPathBuilder + ->associate('bar') + ->associate('baz') + ->create(); + +// Equivalent to +$doctrineOrmAssociationPath = new DoctrineOrmAssociationPath([ + (new DoctrineOrmAssociation('bar')) + ->setAlias('bars') + ->setLoadMode(DoctrineOrmAssociationLoad::FULL), + (new DoctrineOrmAssociation('baz')) + ->setLoadMode(DoctrineOrmAssociationLoad::PROXY), +]); + +$doctrineOrmEntitiesSource = (new DoctrineOrmEntitiesSource([$foo1, $foo2, $foo3])) + ->declareEntityClass(Foo::class); + +$targets = $associate + ->getAdapter('doctrine') + ->getTargets($doctrineOrmEntitiesSource, $doctrineOrmAssociationPath); + +$bazs = $targets->getAll(); + +// Complete usage for prefetching with simplified facad, diverging paths. +$associateDoctrineOrmFetcher->fetch( + $foos, + Foo::class, + [ + ['foo', [ + ['bar'], + ['baz' , 'qux'], + ]], + ['qux'], + ] +); + + +// .foo.bar +// .baz.qux +// .qux +// +$doctrineOrmAssociationPathBuilder + ->diverge() + ->associate('foo') + ->diverge() + ->associate('bar') + ->endDiverge() + ->diverge() + ->associate('baz') + ->associate('qux') + ->endDiverge() + ->endDiverge() + ->diverge() + ->associate('qux') + ->endDiverge(); + +// .foo.bar.baz.qux +// .qux +// +$doctrineOrmAssociationPathBuilder + ->associate('foo') + ->associate('bar') + ->diverge() + ->associate('baz') + ->associate('qux') + ->endDiverge() + ->diverge() + ->associate('qux') + ->endDiverge(); diff --git a/draft-2.php b/draft-2.php new file mode 100644 index 0000000..17544d9 --- /dev/null +++ b/draft-2.php @@ -0,0 +1,14 @@ +bar = $bar; + } + + /** + * @return Bar|null + */ + public function getBar() + { + return $this->bar; + } +} + +class Bar +{ + /** + * @var Baz[] + */ + public $bazs; + + /** + * @param Baz[] $bazs + */ + public function __construct(array $bazs) + { + $this->bazs = $bazs; + } +} + +class Baz +{ + /** + * @var string + */ + protected $text; + + /** + * @param string $text + */ + public function __construct(string $text) + { + $this->text = $text; + } + + /** + * @return string + */ + public function getText(): string + { + return $this->text; + } + + /** + * @return string[] + */ + public function getWords(): array + { + return explode(' ', $this->text); + } + + /** + * @return array + */ + public function getTextStats(): array + { + return ['wordCount' => count($this->getWords())]; + } +} + +$foos = [ + $foo1 = new Foo( + $bar1 = new Bar([ + $baz1 = new Baz('lorem ipsum'), + $baz2 = new Baz('dolor'), + ]) + ), + $foo2 = new Foo( + $bar2 = new Bar([ + $baz1, + $baz3 = new Baz('sit amet malef'), + $baz4 = new Baz('dolor sit'), + ]) + ), + $foo3 = new Foo(), +]; + +$facade = new \Xsolve\Associate\Facade(); +$basicCollector = $facade->getBasicCollector(); + +$bars = $basicCollector->collect($foos, ['bar']); +var_dump($bars); +die; + +$bazs = $basicCollector->collect($foos, ['bar', 'bazs']); +var_dump($bazs); + +$texts = $basicCollector->collect($foos, ['bar', 'bazs', 'text']); +var_dump($texts); + +$words = $basicCollector->collect($foos, ['bar', 'bazs', 'words']); +var_dump($words); + +$wordCounts = $basicCollector->collect($foos, ['bar', 'bazs', 'textStats', '[wordCount]']); +var_dump($wordCounts); diff --git a/lib/AssociationCollecting/AssociationCollectingStrategyInterface.php b/legacyLib/AssociationCollecting/AssociationCollectingStrategyInterface.php similarity index 81% rename from lib/AssociationCollecting/AssociationCollectingStrategyInterface.php rename to legacyLib/AssociationCollecting/AssociationCollectingStrategyInterface.php index e615d4b..fb32c4e 100644 --- a/lib/AssociationCollecting/AssociationCollectingStrategyInterface.php +++ b/legacyLib/AssociationCollecting/AssociationCollectingStrategyInterface.php @@ -1,6 +1,6 @@ classMetadataWrappers)) { - $classMetadata = $this->entityManager->getClassMetadata($className); - - if ($classMetadata instanceof ClassMetadata) { - $entityRepository = $this->entityManager->getRepository($className); - if (!$entityRepository instanceof EntityRepository) { - throw new \Exception(); - } - $this->classMetadataWrappers[$className] = new ClassMetadataWrapper($this, $entityRepository, $classMetadata); - } else { - $this->classMetadataWrappers[$className] = null; - } + $this->initializeClassMetadataWrapperByClassName($className); } return $this->classMetadataWrappers[$className]; } + + /** + * @param string $className + * + * @throws \Exception + */ + protected function initializeClassMetadataWrapperByClassName(string $className): void + { + $classMetadata = $this->entityManager->getClassMetadata($className); + if (!$classMetadata instanceof ClassMetadata) { + $this->classMetadataWrappers[$className] = null; + + return; + } + + $entityRepository = $this->entityManager->getRepository($className); + if (!$entityRepository instanceof EntityRepository) { + throw new \Exception(); + } + $this->classMetadataWrappers[$className] = new ClassMetadataWrapper($this, $entityRepository, $classMetadata); + } } diff --git a/lib/ObjectCollection/NonObjectWrapper.php b/legacyLib/ObjectCollection/NonObjectWrapper.php similarity index 87% rename from lib/ObjectCollection/NonObjectWrapper.php rename to legacyLib/ObjectCollection/NonObjectWrapper.php index e7d0140..fb8b264 100644 --- a/lib/ObjectCollection/NonObjectWrapper.php +++ b/legacyLib/ObjectCollection/NonObjectWrapper.php @@ -1,6 +1,6 @@ setName($categoryData['name']); + $this->categoriesMap[$category->getId()] = $category; + } + + foreach ($data['users'] as $userData) { + $user = new User($userData['id']); + foreach ($userData['stores'] as $storeData) { + $store = new Store($storeData['id']); + $store->setUser($user); + $user->addStore($store); + $store->setApproved($storeData['approved']); + foreach ($storeData['products'] as $productData) { + $product = new Product($productData['id']); + $product->setStore($store); + $store->addProduct($product); + $product->setApproved($productData['approved']); + $product->setName($productData['name']); + $product->setCategory($this->categoriesMap[$productData['categoryId']]); + foreach ($productData['variants'] as $variantData) { + $variant = new Variant($variantData['id']); + $variant->setProduct($product); + $product->addVariant($variant); + $variant->setPrice($variantData['price']); + $this->variantsMap[$variant->getId()] = $variant; + } + $this->productsMap[$product->getId()] = $product; + } + $this->storesMap[$store->getId()] = $store; + } + $this->usersMap[$user->getId()] = $user; + } + } + + /** + * @return User[] + */ + public function getUsers(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->usersMap, + array_flip($ids) + ) + ); + } + + return array_values($this->usersMap); + } + + /** + * @return Store[] + */ + public function getStores(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->storesMap, + array_flip($ids) + ) + ); + } + + return array_values($this->storesMap); + } + + /** + * @return Product[] + */ + public function getProducts(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->productsMap, + array_flip($ids) + ) + ); + } + + return array_values($this->productsMap); + } + + /** + * @return Variant[] + */ + public function getVariants(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->variantsMap, + array_flip($ids) + ) + ); + } + + return array_values($this->variantsMap); + } + + /** + * @return Category[] + */ + public function getCategories(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->categoriesMap, + array_flip($ids) + ) + ); + } + + return array_values($this->categoriesMap); + } + + /** + * @return array + */ + public function getAll(): array + { + return array_merge( + $this->getCategories(), + $this->getUsers(), + $this->getStores(), + $this->getProducts(), + $this->getVariants() + ); + } +} diff --git a/legacyTests/Functional/DoctrineOrm/Mock/Entity/Category.php b/legacyTests/Functional/DoctrineOrm/Mock/Entity/Category.php new file mode 100644 index 0000000..ec8a942 --- /dev/null +++ b/legacyTests/Functional/DoctrineOrm/Mock/Entity/Category.php @@ -0,0 +1,93 @@ +id = $id; + + $this->products = new ArrayCollection(); + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @param string $name + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param Product[] $products + */ + public function setProducts(array $products) + { + $this->products->clear(); + foreach ($products as $product) { + $this->products->add($product); + } + } + + /** + * @return Collection|Product[] + */ + public function getProducts(): Collection + { + return $this->products; + } +} diff --git a/legacyTests/Functional/DoctrineOrm/Mock/Entity/Product.php b/legacyTests/Functional/DoctrineOrm/Mock/Entity/Product.php new file mode 100644 index 0000000..eb29752 --- /dev/null +++ b/legacyTests/Functional/DoctrineOrm/Mock/Entity/Product.php @@ -0,0 +1,171 @@ +id = $id; + + $this->variants = new ArrayCollection(); + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @param string $name + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param bool $approved + */ + public function setApproved(bool $approved) + { + $this->approved = $approved; + } + + /** + * @return bool + */ + public function isApproved(): bool + { + return $this->approved; + } + + /** + * @param Store $store + */ + public function setStore(Store $store) + { + $this->store = $store; + } + + /** + * @return Store + */ + public function getStore(): Store + { + return $this->store; + } + + /** + * @param Category $category + */ + public function setCategory(Category $category) + { + $this->category = $category; + } + + /** + * @return Category + */ + public function getCategory(): Category + { + return $this->category; + } + + /** + * @param Variant[] $variants + */ + public function setVariants(array $variants) + { + $this->variants->clear(); + foreach ($variants as $variant) { + $this->variants->add($variant); + } + } + + /** + * @param Variant $variant + */ + public function addVariant(Variant $variant) + { + $this->variants->add($variant); + } + + /** + * @return Collection|Variant[] + */ + public function getVariants() + { + return $this->variants; + } +} diff --git a/legacyTests/Functional/DoctrineOrm/Mock/Entity/Store.php b/legacyTests/Functional/DoctrineOrm/Mock/Entity/Store.php new file mode 100644 index 0000000..c9ed7ef --- /dev/null +++ b/legacyTests/Functional/DoctrineOrm/Mock/Entity/Store.php @@ -0,0 +1,148 @@ +id = $id; + + $this->products = new ArrayCollection(); + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + +// /** +// * @param string $name +// */ +// public function setName(string $name) +// { +// $this->name = $name; +// } +// +// /** +// * @return string +// */ +// public function getName(): string +// { +// return $this->name; +// } + + /** + * @param bool $approved + */ + public function setApproved(bool $approved) + { + $this->approved = $approved; + } + + /** + * @return bool + */ + public function isApproved(): bool + { + return $this->approved; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + + /** + * @return User + */ + public function getUser(): User + { + return $this->user; + } + + /** + * @param Product[] $products + */ + public function setProducts(array $products) + { + $this->products->clear(); + foreach ($products as $product) { + $this->products->add($product); + } + } + + /** + * @param Product $product + */ + public function addProduct(Product $product) + { + $this->products->add($product); + } + + /** + * @return Collection|Product[] + */ + public function getProducts(): Collection + { + return $this->products; + } +} diff --git a/legacyTests/Functional/DoctrineOrm/Mock/Entity/User.php b/legacyTests/Functional/DoctrineOrm/Mock/Entity/User.php new file mode 100644 index 0000000..65b7937 --- /dev/null +++ b/legacyTests/Functional/DoctrineOrm/Mock/Entity/User.php @@ -0,0 +1,78 @@ +id = $id; + + $this->stores = new ArrayCollection(); + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @param Store[] $stores + */ + public function setStores(array $stores) + { + $this->stores->clear(); + foreach ($stores as $store) { + $this->stores->add($store); + } + } + + /** + * @param Store $store + */ + public function addStore(Store $store) + { + $this->stores->add($store); + } + + /** + * @return Collection|Store[] + */ + public function getStores(): Collection + { + return $this->stores; + } +} diff --git a/legacyTests/Functional/DoctrineOrm/Mock/Entity/Variant.php b/legacyTests/Functional/DoctrineOrm/Mock/Entity/Variant.php new file mode 100644 index 0000000..7a92683 --- /dev/null +++ b/legacyTests/Functional/DoctrineOrm/Mock/Entity/Variant.php @@ -0,0 +1,86 @@ +id = $id; + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @param int $price + */ + public function setPrice(int $price) + { + $this->price = $price; + } + + /** + * @return int + */ + public function getPrice(): int + { + return $this->price; + } + + /** + * @param Product $product + */ + public function setProduct(Product $product) + { + $this->product = $product; + } + + /** + * @return Product + */ + public function getProduct(): Product + { + return $this->product; + } +} diff --git a/legacyTests/Functional/DoctrineOrmHelper.php b/legacyTests/Functional/DoctrineOrmHelper.php new file mode 100644 index 0000000..307c357 --- /dev/null +++ b/legacyTests/Functional/DoctrineOrmHelper.php @@ -0,0 +1,93 @@ + 'pdo_sqlite', + 'dbname' => 'sifter_test', // TODO Move to some config file. + 'user' => 'root', // TODO Move to some config file. + 'password' => '', // TODO Move to some config file. + 'memory' => true, // TODO Move to some config file. + ]; + + $metadataConfiguration = Setup::createAnnotationMetadataConfiguration($entityPaths, $isDevMode); + $this->entityManager = EntityManager::create($databaseOptions, $metadataConfiguration); + } + + /** + * @return EntityManagerInterface + */ + public function getEntityManager(): EntityManagerInterface + { + if (!$this->entityManager instanceof EntityManagerInterface) { + $this->setupEntityManager(); + } + + return $this->entityManager; + } + + /** + * @throws \Doctrine\ORM\Tools\ToolsException + */ + public function createSchema() + { + $this->getEntityManager(); + + $entityClassMetadatas = []; + foreach ($this->getEntityClassNames() as $entityClassName) { + $entityClassMetadatas[] = $this->entityManager->getClassMetadata($entityClassName); + } + + $tool = new SchemaTool($this->entityManager); + $tool->createSchema($entityClassMetadatas); + } + + public function loadData() + { + $this->getEntityManager(); + + $dataProvider = new DataProvider(); + foreach ($dataProvider->getAll() as $entity) { + $this->entityManager->persist($entity); + } + $this->entityManager->flush(); + } + + /** + * @return string[] + */ + protected function getEntityClassNames(): array + { + return [ + Category::class, + User::class, + Store::class, + Product::class, + Variant::class, + ]; + } +} diff --git a/legacyTests/Functional/GenericEntity/Mock/DataProvider.php b/legacyTests/Functional/GenericEntity/Mock/DataProvider.php new file mode 100644 index 0000000..07c7f51 --- /dev/null +++ b/legacyTests/Functional/GenericEntity/Mock/DataProvider.php @@ -0,0 +1,185 @@ +setId($categoryData['id']); + $category->setName($categoryData['name']); + $this->categoriesMap[$category->getId()] = $category; + } + + foreach ($data['users'] as $userData) { + $user = new User(); + $user->setId($userData['id']); + foreach ($userData['stores'] as $storeData) { + $store = new Store(); + $store->setUser($user); + $user->addStore($store); + $store->setId($storeData['id']); + $store->setApproved($storeData['approved']); + foreach ($storeData['products'] as $productData) { + $product = new Product(); + $product->setStore($store); + $store->addProduct($product); + $product->setId($productData['id']); + $product->setApproved($productData['approved']); + $product->setName($productData['name']); + $product->setCategory($this->categoriesMap[$productData['categoryId']]); + foreach ($productData['variants'] as $variantData) { + $variant = new Variant(); + $variant->setProduct($product); + $product->addVariant($variant); + $variant->setId($variantData['id']); + $variant->setPrice($variantData['price']); + $this->variantsMap[$variant->getId()] = $variant; + } + $this->productsMap[$product->getId()] = $product; + } + $this->storesMap[$store->getId()] = $store; + } + $this->usersMap[$user->getId()] = $user; + } + } + + /** + * @return User[] + */ + public function getUsers(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->usersMap, + array_flip($ids) + ) + ); + } + + return array_values($this->usersMap); + } + + /** + * @return Store[] + */ + public function getStores(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->storesMap, + array_flip($ids) + ) + ); + } + + return array_values($this->storesMap); + } + + /** + * @return Product[] + */ + public function getProducts(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->productsMap, + array_flip($ids) + ) + ); + } + + return array_values($this->productsMap); + } + + /** + * @return Variant[] + */ + public function getVariants(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->variantsMap, + array_flip($ids) + ) + ); + } + + return array_values($this->variantsMap); + } + + /** + * @return Category[] + */ + public function getCategories(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->categoriesMap, + array_flip($ids) + ) + ); + } + + return array_values($this->categoriesMap); + } + + /** + * @return array + */ + public function getAll(): array + { + return array_merge( + $this->getCategories(), + $this->getUsers(), + $this->getStores(), + $this->getProducts(), + $this->getVariants() + ); + } +} diff --git a/legacyTests/Functional/GenericEntity/Mock/Model/Category.php b/legacyTests/Functional/GenericEntity/Mock/Model/Category.php new file mode 100644 index 0000000..7383b72 --- /dev/null +++ b/legacyTests/Functional/GenericEntity/Mock/Model/Category.php @@ -0,0 +1,48 @@ +id; + } + + /** + * @param string $id + */ + public function setId(string $id) + { + $this->id = $id; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName(string $name) + { + $this->name = $name; + } +} diff --git a/legacyTests/Functional/GenericEntity/Mock/Model/Product.php b/legacyTests/Functional/GenericEntity/Mock/Model/Product.php new file mode 100644 index 0000000..ebe885a --- /dev/null +++ b/legacyTests/Functional/GenericEntity/Mock/Model/Product.php @@ -0,0 +1,132 @@ +store; + } + + /** + * @param Store $store + */ + public function setStore(Store $store) + { + $this->store = $store; + } + + /** + * @return Variant[] + */ + public function getVariants(): array + { + return $this->variants; + } + + /** + * @param Variant $variant + */ + public function addVariant(Variant $variant) + { + $this->variants[] = $variant; + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @param string $id + */ + public function setId(string $id) + { + $this->id = $id; + } + + /** + * @return bool + */ + public function isApproved(): bool + { + return $this->approved; + } + + /** + * @param bool $approved + */ + public function setApproved(bool $approved) + { + $this->approved = $approved; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return Category + */ + public function getCategory(): Category + { + return $this->category; + } + + /** + * @param Category $category + */ + public function setCategory(Category $category) + { + $this->category = $category; + } +} diff --git a/legacyTests/Functional/GenericEntity/Mock/Model/Store.php b/legacyTests/Functional/GenericEntity/Mock/Model/Store.php new file mode 100644 index 0000000..13f4d88 --- /dev/null +++ b/legacyTests/Functional/GenericEntity/Mock/Model/Store.php @@ -0,0 +1,90 @@ +user; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + + /** + * @return Product[] + */ + public function getProducts(): array + { + return $this->products; + } + + /** + * @param Product $product + */ + public function addProduct(Product $product) + { + $this->products[] = $product; + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @param string $id + */ + public function setId(string $id) + { + $this->id = $id; + } + + /** + * @return bool + */ + public function isApproved(): bool + { + return $this->approved; + } + + /** + * @param bool $approved + */ + public function setApproved(bool $approved) + { + $this->approved = $approved; + } +} diff --git a/legacyTests/Functional/GenericEntity/Mock/Model/User.php b/legacyTests/Functional/GenericEntity/Mock/Model/User.php new file mode 100644 index 0000000..13232fa --- /dev/null +++ b/legacyTests/Functional/GenericEntity/Mock/Model/User.php @@ -0,0 +1,48 @@ +id; + } + + /** + * @param string $id + */ + public function setId(string $id) + { + $this->id = $id; + } + + /** + * @param Store $store + */ + public function addStore(Store $store) + { + $this->stores[] = $store; + } + + /** + * @return Store[] + */ + public function getStores(): array + { + return $this->stores; + } +} diff --git a/legacyTests/Functional/GenericEntity/Mock/Model/Variant.php b/legacyTests/Functional/GenericEntity/Mock/Model/Variant.php new file mode 100644 index 0000000..020f502 --- /dev/null +++ b/legacyTests/Functional/GenericEntity/Mock/Model/Variant.php @@ -0,0 +1,69 @@ +id; + } + + /** + * @param string $id + */ + public function setId(string $id) + { + $this->id = $id; + } + + /** + * @return Product + */ + public function getProduct(): Product + { + return $this->product; + } + + /** + * @param Product $product + */ + public function setProduct(Product $product) + { + $this->product = $product; + } + + /** + * @return int + */ + public function getPrice(): int + { + return $this->price; + } + + /** + * @param int $price + */ + public function setPrice(int $price) + { + $this->price = $price; + } +} diff --git a/tests/Functional/Model/Bar.php b/legacyTests/Functional/Model/Bar.php similarity index 78% rename from tests/Functional/Model/Bar.php rename to legacyTests/Functional/Model/Bar.php index 445604d..ecb8e08 100644 --- a/tests/Functional/Model/Bar.php +++ b/legacyTests/Functional/Model/Bar.php @@ -1,6 +1,6 @@ relationshipName = $relationshipName; + } + + /** + * @return string + */ + public function getRelationshipName(): string + { + return $this->relationshipName; + } + + /** + * @param mixed $alias + * + * @return self + */ + public function setAlias($alias): self + { + $this->alias = $alias; + + return $this; + } + + /** + * @return mixed + */ + public function getAlias() + { + return $this->alias; + } + + /** + * @param string $loadMode + * + * @return self + */ + public function setLoadMode(string $loadMode): self + { + $this->loadMode = $loadMode; + + return $this; + } + + /** + * @return bool + */ + public function isLoadModeFull(): bool + { + return self::LOAD_MODE_FULL === $this->loadMode; + } + + /** + * @return bool + */ + public function isLoadModeProxy(): bool + { + return self::LOAD_MODE_PROXY === $this->loadMode; + } + + /** + * @return bool + */ + public function isLoadModeNone(): bool + { + return self::LOAD_MODE_NONE === $this->loadMode; + } +} diff --git a/lib/DoctrineOrm/Association/Path/AssociationPath.php b/lib/DoctrineOrm/Association/Path/AssociationPath.php new file mode 100644 index 0000000..6bc7857 --- /dev/null +++ b/lib/DoctrineOrm/Association/Path/AssociationPath.php @@ -0,0 +1,27 @@ +associations[] = $association; + } + + /** + * @return Association[] + */ + public function getAssociations(): array + { + return $this->associations; + } +} diff --git a/lib/DoctrineOrm/Association/Path/Builder/AssociationBuilder.php b/lib/DoctrineOrm/Association/Path/Builder/AssociationBuilder.php new file mode 100644 index 0000000..b10268d --- /dev/null +++ b/lib/DoctrineOrm/Association/Path/Builder/AssociationBuilder.php @@ -0,0 +1,109 @@ +association = $association; + $this->parentBuilder = $parentBuilder; + } + + /** + * @param string $alias + * + * @return self + */ + public function aliasAs(string $alias): self + { + $this->association->setAlias($alias); + + return $this; + } + + /** + * @return self + */ + public function loadFull(): self + { + $this->association->setLoadMode(Association::LOAD_MODE_FULL); + + return $this; + } + + /** + * @return self + */ + public function loadProxy(): self + { + $this->association->setLoadMode(Association::LOAD_MODE_PROXY); + + return $this; + } + + /** + * @return self + */ + public function loadNone(): self + { + $this->association->setLoadMode(Association::LOAD_MODE_NONE); + + return $this; + } + + /** + * @param string $relationshipName + * + * @return self + * + * @throws \Exception + */ + public function associate(string $relationshipName): self + { + return $this->parentBuilder->associate($relationshipName); + } + + /** + * @return DivergingAssociationPathBuilder + */ + public function diverge(): DivergingAssociationPathBuilder + { + return $this->parentBuilder->diverge(); + } + + /** + * @return DivergingAssociationPathBuilder + * + * @throws \Exception + */ + public function endDiverge(): DivergingAssociationPathBuilder + { + return $this->parentBuilder->endDiverge(); + } + + /** + * @return DivergingAssociationPath + */ + public function create(): DivergingAssociationPath + { + return $this->parentBuilder->create(); + } +} diff --git a/lib/DoctrineOrm/Association/Path/Builder/DivergingAssociationPathBuilder.php b/lib/DoctrineOrm/Association/Path/Builder/DivergingAssociationPathBuilder.php new file mode 100644 index 0000000..1469979 --- /dev/null +++ b/lib/DoctrineOrm/Association/Path/Builder/DivergingAssociationPathBuilder.php @@ -0,0 +1,92 @@ +divergingAssociationPath = $divergingAssociationPath instanceof DivergingAssociationPath + ? $divergingAssociationPath + : new DivergingAssociationPath(); + $this->parentBuilder = $parentBuilder; + } + + /** + * @param string $relationshipName + * + * @return AssociationBuilder + * + * @throws \Exception + */ + public function associate(string $relationshipName): AssociationBuilder + { + if ($this->divergingAssociationPath->hasChildDivergingAssociationPath()) { + throw new \Exception(); + } + + $association = new Association($relationshipName); + $this->divergingAssociationPath->addAssociation($association); + + return new AssociationBuilder($association, $this); + } + + /** + * @return self + */ + public function diverge(): self + { + $childDivergingAssociationPath = new DivergingAssociationPath(); + $this->divergingAssociationPath->addChildDivergingAsociationPath($childDivergingAssociationPath); + + return new self($childDivergingAssociationPath, $this); + } + + /** + * @return self + * + * @throws \Exception + */ + public function endDiverge(): self + { + if (!$this->parentBuilder instanceof self) { + throw new \Exception(); + } + + return $this->parentBuilder; + } + + /** + * @return DivergingAssociationPath + */ + public function create(): DivergingAssociationPath + { + if ($this->parentBuilder instanceof self) { + return $this->parentBuilder->create(); + } + + return $this->divergingAssociationPath; + } +} diff --git a/lib/DoctrineOrm/Association/Path/DivergingAssociationPath.php b/lib/DoctrineOrm/Association/Path/DivergingAssociationPath.php new file mode 100644 index 0000000..ca23588 --- /dev/null +++ b/lib/DoctrineOrm/Association/Path/DivergingAssociationPath.php @@ -0,0 +1,35 @@ +childDivergingAssociationPaths[] = $childDivergingAssociationPath; + } + + /** + * @return bool + */ + public function hasChildDivergingAssociationPath(): bool + { + return (bool) $this->childDivergingAssociationPaths; + } + + /** + * @return self[] + */ + public function getChildDivergingAssociationPaths(): array + { + return $this->childDivergingAssociationPaths; + } +} diff --git a/lib/DoctrineOrm/Metadata/AssociationMetadataAdapter.php b/lib/DoctrineOrm/Metadata/AssociationMetadataAdapter.php new file mode 100644 index 0000000..11921ac --- /dev/null +++ b/lib/DoctrineOrm/Metadata/AssociationMetadataAdapter.php @@ -0,0 +1,132 @@ +metadataAdapterProvider = $metadataAdapterProvider; + $this->associationMapping = $associationMapping; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->associationMapping['fieldName']; + } + + /** + * @return bool + */ + public function isOwningSide(): bool + { + return $this->associationMapping['isOwningSide']; + } + + /** + * @return bool + */ + public function isInverseSide(): bool + { + return !$this->isOwningSide(); + } + + /** + * @return bool + */ + public function isOneToOne(): bool + { + return ClassMetadataInfo::ONE_TO_ONE === $this->associationMapping['type']; + } + + /** + * @return bool + */ + public function isOneToMany(): bool + { + return ClassMetadataInfo::ONE_TO_MANY === $this->associationMapping['type']; + } + + /** + * @return bool + */ + public function isManyToOne(): bool + { + return ClassMetadataInfo::MANY_TO_ONE === $this->associationMapping['type']; + } + + /** + * @return bool + */ + public function isManyToMany(): bool + { + return ClassMetadataInfo::MANY_TO_MANY === $this->associationMapping['type']; + } + + /** + * @return string + */ + public function getSourceClassName(): string + { + return $this->associationMapping['sourceEntity']; + } + + /** + * @return ClassMetadataAdapter + * + * @throws \Exception + */ + public function getSourceClassMetadataAdapter(): ClassMetadataAdapter + { + $sourceClassName = $this->metadataAdapterProvider->getClassMetadataAdapterByClassName($this->getSourceClassName()); + if (!$sourceClassName instanceof ClassMetadataAdapter) { + throw new \Exception('Source class name not determined.'); + } + + return $sourceClassName; + } + + /** + * @return string + */ + public function getTargetClassName(): string + { + return $this->associationMapping['targetEntity']; + } + + /** + * @return ClassMetadataAdapter + * + * @throws \Exception + */ + public function getTargetClassMetadataAdapter(): ClassMetadataAdapter + { + $targetClassName = $this->metadataAdapterProvider->getClassMetadataAdapterByClassName($this->getTargetClassName()); + if (!$targetClassName instanceof ClassMetadataAdapter) { + throw new \Exception('Target class name not determined.'); + } + + return $targetClassName; + } +} diff --git a/lib/DoctrineOrm/Metadata/ClassMetadataAdapter.php b/lib/DoctrineOrm/Metadata/ClassMetadataAdapter.php new file mode 100644 index 0000000..0806ade --- /dev/null +++ b/lib/DoctrineOrm/Metadata/ClassMetadataAdapter.php @@ -0,0 +1,153 @@ +metadataAdapterProvider = $metadataAdapterProvider; + $this->repository = $repository; + $this->classMetadata = $classMetadata; + } + + /** + * @return string + */ + public function getClassName(): string + { + return $this->classMetadata->getName(); + } + + /** + * @return string + */ + public function getRootClassName(): string + { + return $this->classMetadata->rootEntityName; + } + + /** + * @param string $rootAlias + * + * @return QueryBuilder + */ + public function createQueryBuilder(string $rootAlias): QueryBuilder + { + return $this->repository->createQueryBuilder($rootAlias); + } + + /** + * @param object $object + * + * @return mixed + * + * @throws \Exception + */ + public function getIdentifierValueForOne(object $object) + { + $identifierValues = $this->classMetadata->getIdentifierValues($object); + + return $identifierValues[$this->getIdentifierFieldName()]; + } + + /** + * @param object[] $objects + * + * @return array + */ + public function getIdentifierValueForMultiple(array $objects): array + { + return array_map( + function (object $object) { + return $this->getIdentifierValueForOne($object); + }, + $objects + ); + } + + /** + * @param string $associationName + * + * @return AssociationMetadataAdapter|null + * + * @throws MappingException + */ + public function getAssociationMetadataAdapter(string $associationName): ?AssociationMetadataAdapter + { + if (!array_key_exists($associationName, $this->associationMetadataAdapters)) { + try { + $associationMapping = $this->classMetadata->getAssociationMapping($associationName); + } catch (MappingException $e) { + if (0 === strpos($e->getMessage(), 'No mapping found for field ')) { + return null; + } + throw $e; + } + $this->associationMetadataAdapters[$associationName] = new AssociationMetadataAdapter( + $this->metadataAdapterProvider, + $associationMapping + ); + } + + return $this->associationMetadataAdapters[$associationName]; + } + + /** + * @return string + * + * @throws \Exception + */ + public function getIdentifierFieldName(): string + { + if (is_null($this->identifierFieldName)) { + $identifierFieldNames = $this->classMetadata->getIdentifierFieldNames(); + + if (1 !== count($identifierFieldNames)) { + throw new \Exception('Composite primary keys are not supported.'); + } + + $this->identifierFieldName = reset($identifierFieldNames); + } + + return $this->identifierFieldName; + } +} diff --git a/lib/DoctrineOrm/Metadata/MetadataAdapterProvider.php b/lib/DoctrineOrm/Metadata/MetadataAdapterProvider.php new file mode 100644 index 0000000..864b500 --- /dev/null +++ b/lib/DoctrineOrm/Metadata/MetadataAdapterProvider.php @@ -0,0 +1,120 @@ +entityManager = $entityManager; + } + +// /** +// * @param array $objects +// * +// * @return ClassMetadataAdapter +// * +// * @throws \Exception +// */ +// public function getClassMetadataAdapterForEntities(array $objects): ClassMetadataAdapter +// { +// $className = $this->getClassNameForEntities($objects); +// $classMetadataAdapter = $this->getClassMetadataAdapterByClassName($className); +// if (!$classMetadataAdapter instanceof ClassMetadataAdapter) { +// throw new \Exception(); +// } +// +// return $classMetadataAdapter; +// } + + /** + * @param array $objects + * + * @return string + * + * @throws \Exception + */ + public function getClassNameForEntities(array $objects): string + { + if (!$objects) { + throw new \Exception(); + } + + $firstObject = array_shift($objects); + $classMetadataAdapter = $this->getClassMetadataAdapterByClassName(get_class($firstObject)); + if (!$classMetadataAdapter instanceof ClassMetadataAdapter) { + throw new \Exception(); + } + $commonClassName = $classMetadataAdapter->getClassName(); + $rootClassNameUsed = false; + + foreach ($objects as $object) { + if (is_a($object, $commonClassName, true)) { + continue; + } + if ($rootClassNameUsed) { + throw new \Exception(); + } + $commonClassName = $classMetadataAdapter->getRootClassName(); + if (!is_a($object, $commonClassName, true)) { + throw new \Exception(); + } + } + + return $commonClassName; + } + + /** + * @param string $className + * + * @return ClassMetadataAdapter|null + * + * @throws \Exception + */ + public function getClassMetadataAdapterByClassName(string $className): ?ClassMetadataAdapter + { + if (!array_key_exists($className, $this->classMetadataAdapters)) { + $this->initializeClassMetadataAdapterByClassName($className); + } + + return $this->classMetadataAdapters[$className]; + } + + /** + * @param string $className + * + * @throws \Exception + */ + protected function initializeClassMetadataAdapterByClassName(string $className): void + { + $classMetadata = $this->entityManager->getClassMetadata($className); + if (!$classMetadata instanceof ClassMetadata) { + $this->classMetadataAdapters[$className] = null; + + return; + } + + $entityRepository = $this->entityManager->getRepository($className); + if (!$entityRepository instanceof EntityRepository) { + throw new \Exception(); + } + $this->classMetadataAdapters[$className] = new ClassMetadataAdapter($this, $entityRepository, $classMetadata); + } +} diff --git a/lib/DoctrineOrm/Source/EntityClassHelper.php b/lib/DoctrineOrm/Source/EntityClassHelper.php new file mode 100644 index 0000000..d70ea3e --- /dev/null +++ b/lib/DoctrineOrm/Source/EntityClassHelper.php @@ -0,0 +1,43 @@ +metadataAdapterProvider = $metadataAdapterProvider; + } + + /** + * @param EntitySource $entitySource + * + * @throws \Exception if one common entity class cannot be determined + */ + public function supplementEntitySource(EntitySource $entitySource): void + { + if ($entitySource->hasEntityClass()) { + return; + } + + $entityClassName = $this->metadataAdapterProvider->getClassNameForEntities($entitySource->getEntities()); + $entitySource->setEntityClass($entityClassName); + + $classMetadataAdapter = $this->metadataAdapterProvider->getClassMetadataAdapterByClassName($entityClassName); + if (!$classMetadataAdapter instanceof ClassMetadataAdapter) { + throw new \Exception(); + } + $entitySource->setClassMetadataAdapter($classMetadataAdapter); + } +} diff --git a/lib/DoctrineOrm/Source/EntitySource.php b/lib/DoctrineOrm/Source/EntitySource.php new file mode 100644 index 0000000..4993d5c --- /dev/null +++ b/lib/DoctrineOrm/Source/EntitySource.php @@ -0,0 +1,81 @@ +entities = $entities; + $this->entityClass = $declaredEntityClass; + } + + /** + * @return array + */ + public function getEntities(): array + { + return $this->entities; + } + + /** + * @param string $entityClass + */ + public function setEntityClass(string $entityClass): void + { + $this->entityClass = $entityClass; + } + + /** + * @return bool + */ + public function hasEntityClass(): bool + { + return null !== $this->entityClass; + } + + /** + * @return string|null + */ + public function getEntityClass(): ?string + { + return $this->entityClass; + } + + /** + * @param ClassMetadataAdapter $classMetadataAdapter + */ + public function setClassMetadataAdapter(ClassMetadataAdapter $classMetadataAdapter): void + { + $this->classMetadataAdapter = $classMetadataAdapter; + } + + /** + * @return ClassMetadataAdapter|null + */ + public function getClassMetadataAdapter(): ?ClassMetadataAdapter + { + return $this->classMetadataAdapter; + } +} diff --git a/lib/DoctrineOrm/Source/IdentifierSource.php b/lib/DoctrineOrm/Source/IdentifierSource.php new file mode 100644 index 0000000..d0d3084 --- /dev/null +++ b/lib/DoctrineOrm/Source/IdentifierSource.php @@ -0,0 +1,42 @@ +identifiers = $identifiers; + $this->entityClass = $entityClass; + } + + /** + * @return array + */ + public function getIdentifiers(): array + { + return $this->identifiers; + } + + /** + * @return string + */ + public function getEntityClass(): string + { + return $this->entityClass; + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 62f2553..f36331a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,6 +12,7 @@ ./ ./tests + ./legacyTests ./vendor diff --git a/sandbox-1.php b/sandbox-1.php new file mode 100755 index 0000000..c1fca00 --- /dev/null +++ b/sandbox-1.php @@ -0,0 +1,75 @@ +#!/usr/bin/php +associate('bar') + ->aliasAs('bars') + ->loadFull() + ->associate('baz') + ->associate('qux') + ->aliasAs('qux') + ->loadProxy() + ->create(); + +dump($doctrineOrmAssociationPath); + +$doctrineOrmAssociationPathBuilder = new DivergingAssociationPathBuilder(); + +$doctrineOrmAssociationPath = $doctrineOrmAssociationPathBuilder + ->associate('bar') + ->associate('baz') + ->associate('qux') + ->loadFull() + ->create(); + +dump($doctrineOrmAssociationPath); + +// .foo.bar +// .baz.qux +// .qux +// +$doctrineOrmAssociationPathBuilder = new DivergingAssociationPathBuilder(); + +$doctrineOrmAssociationPath = $doctrineOrmAssociationPathBuilder + ->diverge() + ->associate('foo') + ->diverge() + ->associate('bar') + ->endDiverge() + ->diverge() + ->associate('baz') + ->aliasAs('.foo.baz') + ->loadProxy() + ->associate('qux') + ->endDiverge() + ->endDiverge() + ->diverge() + ->associate('qux') + ->endDiverge() + ->create(); + +dump($doctrineOrmAssociationPath); + +// .foo.bar.baz.qux +// .qux +// +$doctrineOrmAssociationPathBuilder = new DivergingAssociationPathBuilder(); + +$doctrineOrmAssociationPath = $doctrineOrmAssociationPathBuilder + ->associate('foo') + ->associate('bar') + ->diverge() + ->associate('baz') + ->associate('qux') + ->endDiverge() + ->diverge() + ->associate('qux') + ->endDiverge(); + +dump($doctrineOrmAssociationPath); diff --git a/tests/Functional/DoctrineOrm/Association/Path/Builder/DivergingAssociationPathBuilderTest.php b/tests/Functional/DoctrineOrm/Association/Path/Builder/DivergingAssociationPathBuilderTest.php new file mode 100644 index 0000000..e7535f3 --- /dev/null +++ b/tests/Functional/DoctrineOrm/Association/Path/Builder/DivergingAssociationPathBuilderTest.php @@ -0,0 +1,202 @@ +associate('bar') + ->aliasAs('bars') + ->loadFull() + ->associate('baz') + ->associate('qux') + ->aliasAs('qux') + ->loadProxy() + ->create(); + + $this->assertInstanceOf(DivergingAssociationPath::class, $divergingAssociationPath); + $this->assertSame( + [ + '.bar.baz.qux', + ], + $this->convertDivergingAssociationPathToStrings($divergingAssociationPath) + ); + } + + public function testCase2() + { + $divergingAssociationPathBuilder = new DivergingAssociationPathBuilder(); + + $divergingAssociationPath = $divergingAssociationPathBuilder + ->associate('bar') + ->associate('baz') + ->associate('qux') + ->loadFull() + ->create(); + + $this->assertInstanceOf(DivergingAssociationPath::class, $divergingAssociationPath); + $this->assertSame( + [ + '.bar.baz.qux', + ], + $this->convertDivergingAssociationPathToStrings($divergingAssociationPath) + ); + } + + public function testCase3() + { + $divergingAssociationPathBuilder = new DivergingAssociationPathBuilder(); + + $divergingAssociationPath = $divergingAssociationPathBuilder + ->diverge() + ->associate('foo') + ->diverge() + ->associate('bar') + ->endDiverge() + ->diverge() + ->associate('baz') + ->aliasAs('.foo.baz') + ->loadProxy() + ->associate('qux') + ->endDiverge() + ->endDiverge() + ->diverge() + ->associate('qux') + ->endDiverge() + ->create(); + + $this->assertInstanceOf(DivergingAssociationPath::class, $divergingAssociationPath); + $this->assertSame( + [ + '.foo.bar', + '.foo.baz.qux', + '.qux', + ], + $this->convertDivergingAssociationPathToStrings($divergingAssociationPath) + ); + } + + public function testCase4() + { + $divergingAssociationPathBuilder = new DivergingAssociationPathBuilder(); + + $divergingAssociationPath = $divergingAssociationPathBuilder + ->associate('foo') + ->associate('bar') + ->diverge() + ->associate('baz') + ->associate('qux') + ->endDiverge() + ->diverge() + ->associate('qux') + ->endDiverge() + ->create(); + + $this->assertInstanceOf(DivergingAssociationPath::class, $divergingAssociationPath); + $this->assertSame( + [ + '.foo.bar.baz.qux', + '.foo.bar.qux', + ], + $this->convertDivergingAssociationPathToStrings($divergingAssociationPath) + ); + } + + public function testCase5() + { + $divergingAssociationPathBuilder = new DivergingAssociationPathBuilder(); + + $divergingAssociationPath = $divergingAssociationPathBuilder + ->diverge() + ->associate('foo') + ->endDiverge() + ->diverge() + ->associate('bar') + ->endDiverge() + ->diverge() + ->associate('baz') + ->associate('qux') + ->endDiverge() + ->diverge() + ->associate('qux') + ->endDiverge() + ->create(); + + $this->assertInstanceOf(DivergingAssociationPath::class, $divergingAssociationPath); + $this->assertSame( + [ + '.foo', + '.bar', + '.baz.qux', + '.qux', + ], + $this->convertDivergingAssociationPathToStrings($divergingAssociationPath) + ); + } + + public function testCase6() + { + $divergingAssociationPathBuilder = new DivergingAssociationPathBuilder(); + + $divergingAssociationPath = $divergingAssociationPathBuilder + ->diverge() + ->associate('foo') + ->diverge() + ->associate('bar') + ->diverge() + ->associate('baz') + ->associate('qux') + ->diverge() + ->associate('qux') + ->create(); + + $this->assertInstanceOf(DivergingAssociationPath::class, $divergingAssociationPath); + $this->assertSame( + [ + '.foo.bar.baz.qux.qux', + ], + $this->convertDivergingAssociationPathToStrings($divergingAssociationPath) + ); + } + + /** + * @param DivergingAssociationPath $divergingAssociationPath + * + * @return string[] + */ + protected function convertDivergingAssociationPathToStrings(DivergingAssociationPath $divergingAssociationPath): array + { + $path = ''; + foreach ($divergingAssociationPath->getAssociations() as $association) { + $path .= sprintf('.%s', $association->getRelationshipName()); + } + + if (!$divergingAssociationPath->hasChildDivergingAssociationPath()) { + return [$path]; + } + + $childPaths = []; + foreach ($divergingAssociationPath->getChildDivergingAssociationPaths() as $childDivergingAssociationPath) { + $childPaths = array_merge( + $childPaths, + $this->convertDivergingAssociationPathToStrings($childDivergingAssociationPath) + ); + } + foreach (array_keys($childPaths) as $childPathIndex) { + $childPaths[$childPathIndex] = $path . $childPaths[$childPathIndex]; + } + + return $childPaths; + } +} diff --git a/tests/Functional/DoctrineOrm/Mock/DataProvider.php b/tests/Functional/DoctrineOrm/Mock/DataProvider.php new file mode 100644 index 0000000..17ade8e --- /dev/null +++ b/tests/Functional/DoctrineOrm/Mock/DataProvider.php @@ -0,0 +1,179 @@ +setName($categoryData['name']); + $this->categoriesMap[$category->getId()] = $category; + } + + foreach ($data['users'] as $userData) { + $user = new User($userData['id']); + foreach ($userData['stores'] as $storeData) { + $store = new Store($storeData['id']); + $store->setUser($user); + $user->addStore($store); + $store->setApproved($storeData['approved']); + foreach ($storeData['products'] as $productData) { + $product = new Product($productData['id']); + $product->setStore($store); + $store->addProduct($product); + $product->setApproved($productData['approved']); + $product->setName($productData['name']); + $product->setCategory($this->categoriesMap[$productData['categoryId']]); + foreach ($productData['variants'] as $variantData) { + $variant = new Variant($variantData['id']); + $variant->setProduct($product); + $product->addVariant($variant); + $variant->setPrice($variantData['price']); + $this->variantsMap[$variant->getId()] = $variant; + } + $this->productsMap[$product->getId()] = $product; + } + $this->storesMap[$store->getId()] = $store; + } + $this->usersMap[$user->getId()] = $user; + } + } + + /** + * @return User[] + */ + public function getUsers(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->usersMap, + array_flip($ids) + ) + ); + } + + return array_values($this->usersMap); + } + + /** + * @return Store[] + */ + public function getStores(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->storesMap, + array_flip($ids) + ) + ); + } + + return array_values($this->storesMap); + } + + /** + * @return Product[] + */ + public function getProducts(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->productsMap, + array_flip($ids) + ) + ); + } + + return array_values($this->productsMap); + } + + /** + * @return Variant[] + */ + public function getVariants(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->variantsMap, + array_flip($ids) + ) + ); + } + + return array_values($this->variantsMap); + } + + /** + * @return Category[] + */ + public function getCategories(array $ids = null): array + { + if (is_array($ids)) { + return array_values( + array_intersect_key( + $this->categoriesMap, + array_flip($ids) + ) + ); + } + + return array_values($this->categoriesMap); + } + + /** + * @return array + */ + public function getAll(): array + { + return array_merge( + $this->getCategories(), + $this->getUsers(), + $this->getStores(), + $this->getProducts(), + $this->getVariants() + ); + } +} diff --git a/tests/Functional/DoctrineOrm/Mock/DataProviderInterface.php b/tests/Functional/DoctrineOrm/Mock/DataProviderInterface.php new file mode 100644 index 0000000..2feaea4 --- /dev/null +++ b/tests/Functional/DoctrineOrm/Mock/DataProviderInterface.php @@ -0,0 +1,36 @@ + 'pdo_sqlite', + 'dbname' => 'sifter_test', // TODO Move to some config file. + 'user' => 'root', // TODO Move to some config file. + 'password' => '', // TODO Move to some config file. + 'memory' => true, // TODO Move to some config file. + ]; + + $metadataConfiguration = Setup::createAnnotationMetadataConfiguration($entityPaths, $isDevMode); + $this->entityManager = EntityManager::create($databaseOptions, $metadataConfiguration); + } + + /** + * @return EntityManagerInterface + */ + public function getEntityManager(): EntityManagerInterface + { + if (!$this->entityManager instanceof EntityManagerInterface) { + $this->setupEntityManager(); + } + + return $this->entityManager; + } + + /** + * @throws \Doctrine\ORM\Tools\ToolsException + */ + public function createSchema() + { + $this->getEntityManager(); + + $entityClassMetadatas = []; + foreach ($this->getEntityClassNames() as $entityClassName) { + $entityClassMetadatas[] = $this->entityManager->getClassMetadata($entityClassName); + } + + $tool = new SchemaTool($this->entityManager); + $tool->createSchema($entityClassMetadatas); + } + + public function loadData() + { + $this->getEntityManager(); + + $dataProvider = new DataProvider(); + foreach ($dataProvider->getAll() as $entity) { + $this->entityManager->persist($entity); + } + $this->entityManager->flush(); + } + + /** + * @return string[] + */ + protected function getEntityClassNames(): array + { + return [ + Category::class, + User::class, + Store::class, + Product::class, + Variant::class, + ]; + } +} diff --git a/tests/Functional/DoctrineOrm/Mock/Entity/Category.php b/tests/Functional/DoctrineOrm/Mock/Entity/Category.php new file mode 100644 index 0000000..47a8467 --- /dev/null +++ b/tests/Functional/DoctrineOrm/Mock/Entity/Category.php @@ -0,0 +1,93 @@ +id = $id; + + $this->products = new ArrayCollection(); + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @param string $name + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param Product[] $products + */ + public function setProducts(array $products) + { + $this->products->clear(); + foreach ($products as $product) { + $this->products->add($product); + } + } + + /** + * @return Collection|Product[] + */ + public function getProducts(): Collection + { + return $this->products; + } +} diff --git a/tests/Functional/DoctrineOrm/Mock/Entity/Product.php b/tests/Functional/DoctrineOrm/Mock/Entity/Product.php new file mode 100644 index 0000000..7e1444f --- /dev/null +++ b/tests/Functional/DoctrineOrm/Mock/Entity/Product.php @@ -0,0 +1,171 @@ +id = $id; + + $this->variants = new ArrayCollection(); + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @param string $name + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param bool $approved + */ + public function setApproved(bool $approved) + { + $this->approved = $approved; + } + + /** + * @return bool + */ + public function isApproved(): bool + { + return $this->approved; + } + + /** + * @param Store $store + */ + public function setStore(Store $store) + { + $this->store = $store; + } + + /** + * @return Store + */ + public function getStore(): Store + { + return $this->store; + } + + /** + * @param Category $category + */ + public function setCategory(Category $category) + { + $this->category = $category; + } + + /** + * @return Category + */ + public function getCategory(): Category + { + return $this->category; + } + + /** + * @param Variant[] $variants + */ + public function setVariants(array $variants) + { + $this->variants->clear(); + foreach ($variants as $variant) { + $this->variants->add($variant); + } + } + + /** + * @param Variant $variant + */ + public function addVariant(Variant $variant) + { + $this->variants->add($variant); + } + + /** + * @return Collection|Variant[] + */ + public function getVariants() + { + return $this->variants; + } +} diff --git a/tests/Functional/DoctrineOrm/Mock/Entity/Store.php b/tests/Functional/DoctrineOrm/Mock/Entity/Store.php new file mode 100644 index 0000000..a4032eb --- /dev/null +++ b/tests/Functional/DoctrineOrm/Mock/Entity/Store.php @@ -0,0 +1,148 @@ +id = $id; + + $this->products = new ArrayCollection(); + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + +// /** +// * @param string $name +// */ +// public function setName(string $name) +// { +// $this->name = $name; +// } +// +// /** +// * @return string +// */ +// public function getName(): string +// { +// return $this->name; +// } + + /** + * @param bool $approved + */ + public function setApproved(bool $approved) + { + $this->approved = $approved; + } + + /** + * @return bool + */ + public function isApproved(): bool + { + return $this->approved; + } + + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + } + + /** + * @return User + */ + public function getUser(): User + { + return $this->user; + } + + /** + * @param Product[] $products + */ + public function setProducts(array $products) + { + $this->products->clear(); + foreach ($products as $product) { + $this->products->add($product); + } + } + + /** + * @param Product $product + */ + public function addProduct(Product $product) + { + $this->products->add($product); + } + + /** + * @return Collection|Product[] + */ + public function getProducts(): Collection + { + return $this->products; + } +} diff --git a/tests/Functional/DoctrineOrm/Mock/Entity/User.php b/tests/Functional/DoctrineOrm/Mock/Entity/User.php new file mode 100644 index 0000000..73671c9 --- /dev/null +++ b/tests/Functional/DoctrineOrm/Mock/Entity/User.php @@ -0,0 +1,78 @@ +id = $id; + + $this->stores = new ArrayCollection(); + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @param Store[] $stores + */ + public function setStores(array $stores) + { + $this->stores->clear(); + foreach ($stores as $store) { + $this->stores->add($store); + } + } + + /** + * @param Store $store + */ + public function addStore(Store $store) + { + $this->stores->add($store); + } + + /** + * @return Collection|Store[] + */ + public function getStores(): Collection + { + return $this->stores; + } +} diff --git a/tests/Functional/DoctrineOrm/Mock/Entity/Variant.php b/tests/Functional/DoctrineOrm/Mock/Entity/Variant.php new file mode 100644 index 0000000..3ffcfd8 --- /dev/null +++ b/tests/Functional/DoctrineOrm/Mock/Entity/Variant.php @@ -0,0 +1,86 @@ +id = $id; + } + + /** + * @return string + */ + public function getId(): string + { + return $this->id; + } + + /** + * @param int $price + */ + public function setPrice(int $price) + { + $this->price = $price; + } + + /** + * @return int + */ + public function getPrice(): int + { + return $this->price; + } + + /** + * @param Product $product + */ + public function setProduct(Product $product) + { + $this->product = $product; + } + + /** + * @return Product + */ + public function getProduct(): Product + { + return $this->product; + } +} diff --git a/tests/Functional/DoctrineOrm/Mock/data.yml b/tests/Functional/DoctrineOrm/Mock/data.yml new file mode 100644 index 0000000..077ce58 --- /dev/null +++ b/tests/Functional/DoctrineOrm/Mock/data.yml @@ -0,0 +1,220 @@ +categories: + - + id: c1 + name: 'Category 1' + - + id: c2 + name: 'Category 2' + - + id: c3 + name: 'Category 3' + +users: + - + id: u1 + stores: + - + id: s1 + approved: false + products: + - + id: s1p1 + approved: false + name: Lorem + categoryId: c1 + variants: + - + id: s1p1v1 + price: 999 + - + id: s1p1v2 + price: 120 + - + id: s1p1v3 + price: 121 + - + id: s1p1v4 + price: 160 + - + id: s1p2 + approved: true + name: Ipsum + categoryId: c2 + variants: + - + id: s1p2v1 + price: 100 + - + id: s1p2v2 + price: 220 + - + id: s1p2v3 + price: 300 + - + id: s1p2v4 + price: 500 + - + id: s1p3 + approved: true + name: Ipsum + categoryId: c1 + variants: + - + id: s1p3v1 + price: 10 + - + id: s1p3v2 + price: 500 + - + id: s2 + approved: false + products: + - + id: s2p1 + approved: true + name: Sit + categoryId: c3 + variants: + - + id: s2p1v1 + price: 600 + - + id: s2p1v2 + price: 520 + - + id: s2p1v3 + price: 321 + - + id: s2p1v4 + price: 460 + - + id: s2p2 + approved: false + name: Amet + categoryId: c2 + variants: + - + id: s2p2v1 + price: 500 + - + id: s2p2v2 + price: 300 + - + id: s2p2v3 + price: 200 + - + id: s2p2v4 + price: 100 + - + id: s2p3 + approved: true + name: Nullam + categoryId: c1 + variants: + - + id: s2p3v1 + price: 10 + - + id: u2 + stores: + - + id: s3 + approved: true + products: + - + id: s3p1 + approved: false + name: Ipsum + categoryId: c2 + variants: + - + id: s3p1v1 + price: 100 + - + id: s3p1v2 + price: 120 + - + id: s3p1v3 + price: 121 + - + id: s3p1v4 + price: 160 + - + id: s3p2 + approved: true + name: Lorem + categoryId: c2 + variants: + - + id: s3p2v1 + price: 100 + - + id: s3p2v2 + price: 220 + - + id: s3p2v3 + price: 300 + - + id: s3p2v4 + price: 500 + - + id: s3p3 + approved: true + name: Qui + categoryId: c1 + variants: + - + id: s3p3v1 + price: 10 + - + id: s3p3v2 + price: 500 + - + id: s4 + approved: true + products: + - + id: s4p1 + approved: true + name: Vestibulum + categoryId: c3 + variants: + - + id: s4p1v1 + price: 600 + - + id: s4p1v2 + price: 520 + - + id: s4p1v3 + price: 321 + - + id: s4p1v4 + price: 460 + - + id: s4p2 + approved: true + name: Amet + categoryId: c2 + variants: + - + id: s4p2v1 + price: 500 + - + id: s4p2v2 + price: 300 + - + id: s4p2v3 + price: 200 + - + id: s4p2v4 + price: 100 + - + id: s4p3 + approved: false + name: Nullam + categoryId: c3 + variants: + - + id: s4p3v1 + price: 10 diff --git a/tests/Functional/DoctrineOrm/Source/EntityClassHelperTest.php b/tests/Functional/DoctrineOrm/Source/EntityClassHelperTest.php new file mode 100644 index 0000000..0dbf569 --- /dev/null +++ b/tests/Functional/DoctrineOrm/Source/EntityClassHelperTest.php @@ -0,0 +1,131 @@ +entityManager = $doctrineOrmHelper->getEntityManager(); + + $metadataAdapterProvider = new MetadataAdapterProvider($this->entityManager); + $entityClassHelper = new EntityClassHelper($metadataAdapterProvider); + + $entitySource = new EntitySource($entities); + $entityClassHelper->supplementEntitySource($entitySource); + + $this->assertEquals($expectedClassName, $entitySource->getEntityClass()); + + $this->assertContains($expectedClassName, [ + $entitySource->getClassMetadataAdapter()->getClassName(), + $entitySource->getClassMetadataAdapter()->getRootClassName(), + ]); + } + + /** + * @return array + */ + public function dataSetEntityClass(): array + { + $this->dataProvider = new DataProvider(); + + return [ + [ + $this->dataProvider->getProducts(['s1p1', 's1p3', 's3p1']), + Product::class, + ], + [ + $this->dataProvider->getStores(['s1', 's2']), + Store::class, + ], + [ + $this->dataProvider->getVariants(['s3p1v1']), + Variant::class, + ], + ]; + } + + /** + * @param object[] $entities + * + * @dataProvider dataSetEntityClassIfNoCommonClassName + * @expectedException \Exception + */ + public function testSetEntityClassIfNoCommonClassName(array $entities): void + { + $doctrineOrmHelper = new DoctrineOrmHelper(); + $this->entityManager = $doctrineOrmHelper->getEntityManager(); + + $metadataAdapterProvider = new MetadataAdapterProvider($this->entityManager); + $entityClassHelper = new EntityClassHelper($metadataAdapterProvider); + + $entitySource = new EntitySource($entities); + $entityClassHelper->supplementEntitySource($entitySource); + } + + /** + * @return array + */ + public function dataSetEntityClassIfNoCommonClassName(): array + { + $this->dataProvider = new DataProvider(); + + return [ + [ + [ + $this->dataProvider->getProducts(['s1p1'])[0], + $this->dataProvider->getStores(['s2'])[0], + $this->dataProvider->getProducts(['s3p1'])[0], + ], + ], + [ + [ + $this->dataProvider->getStores(['s1'])[0], + $this->dataProvider->getStores(['s2'])[0], + $this->dataProvider->getProducts(['s1p3'])[0], + ], + ], + [ + [ + $this->dataProvider->getVariants(['s3p1v1'])[0], + $this->dataProvider->getStores(['s1'])[0], + $this->dataProvider->getProducts(['s2p1'])[0], + ], + ], + [ + [ + new \stdClass(), + new \stdClass(), + ], + ], + ]; + } +}