diff --git a/composer.json b/composer.json index e4f05f5..bf1ddf9 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "ext-ctype": "*", "ext-iconv": "*", "api-platform/core": "^3.2", + "beberlei/doctrineextensions": "^1.4", "brick/math": "^0.11.0", "brick/money": "^0.8.1", "doctrine/doctrine-bundle": "^2.10", diff --git a/composer.lock b/composer.lock index 9e9a0c9..f350753 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e65a6d86be5982961b544a7bfa7704c9", + "content-hash": "4c6f6e7a3a4506e1cede90bd2a00e33a", "packages": [ { "name": "api-platform/core", @@ -172,6 +172,66 @@ }, "time": "2024-02-20T09:52:06+00:00" }, + { + "name": "beberlei/doctrineextensions", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/beberlei/DoctrineExtensions.git", + "reference": "249eab82aa35b65741388f38499b3162403d9956" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/DoctrineExtensions/zipball/249eab82aa35b65741388f38499b3162403d9956", + "reference": "249eab82aa35b65741388f38499b3162403d9956", + "shasum": "" + }, + "require": { + "doctrine/orm": "^2.15", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^1.14 || ^2", + "doctrine/coding-standard": "^9.0.2 || ^12.0", + "nesbot/carbon": "*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.8", + "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0", + "symfony/yaml": "^4.4 || ^5.3 || ^6.0 || ^7.0", + "zf1/zend-date": "^1.12", + "zf1/zend-registry": "^1.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "DoctrineExtensions\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Steve Lacey", + "email": "steve@steve.ly" + } + ], + "description": "A set of extensions to Doctrine 2 that add support for additional query functions available in MySQL, Oracle, PostgreSQL and SQLite.", + "keywords": [ + "database", + "doctrine", + "orm" + ], + "support": { + "source": "https://github.com/beberlei/DoctrineExtensions/tree/v1.4.0" + }, + "time": "2024-02-05T17:02:44+00:00" + }, { "name": "behat/transliterator", "version": "v1.5.0", diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index a10c213..6bd17eb 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -27,6 +27,9 @@ doctrine: prefix: Gedmo\Loggable\Entity dir: "%kernel.project_dir%/vendor/gedmo/doctrine-extensions/src/Loggable/Entity" is_bundle: false + dql: + string_functions: + instr: DoctrineExtensions\Query\Mysql\Instr when@test: doctrine: diff --git a/src/ApiResource/Version.php b/src/ApiResource/Version.php index 1ee3550..c899edf 100644 --- a/src/ApiResource/Version.php +++ b/src/ApiResource/Version.php @@ -63,7 +63,7 @@ class Version /** * The ID of the recorded resource. */ - public function getResouceId(): int + public function getResourceId(): int { return $this->log->getObjectId(); } diff --git a/src/Command/BenzinaPumpCommand.php b/src/Command/BenzinaPumpCommand.php index 796de05..177d197 100644 --- a/src/Command/BenzinaPumpCommand.php +++ b/src/Command/BenzinaPumpCommand.php @@ -90,7 +90,6 @@ EOF }, $pumps)); $io->writeln(sprintf("Processing %d records.", $streamSize)); - $progress = $io->createProgressBar(); $progress->start($streamSize); @@ -107,6 +106,7 @@ EOF $stream->close(); $progress->finish(); + $io->writeln("\n"); $io->success("Data processed successfully!"); return Command::SUCCESS; diff --git a/src/Entity/User.php b/src/Entity/User.php index b89128d..2fe3e97 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -2,9 +2,10 @@ namespace App\Entity; -use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Metadata as API; use App\Entity\Interface\UserOwnedInterface; +use App\Filter\OrderedLikeFilter; +use App\Filter\UserQueryFilter; use App\Repository\UserRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -29,10 +30,8 @@ use Symfony\Component\Validator\Constraints as Assert; #[API\Put(security: 'is_granted("USER_EDIT", object)')] #[API\Delete(security: 'is_granted("USER_EDIT", object)')] #[API\Patch(security: 'is_granted("USER_EDIT", object)')] -#[API\ApiFilter(filterClass: SearchFilter::class, properties: [ - 'username' => 'partial', - 'name' => 'partial' -])] +#[API\ApiFilter(filterClass: UserQueryFilter::class, properties: ['query'])] +#[API\ApiFilter(filterClass: OrderedLikeFilter::class, properties: ['username'])] #[UniqueEntity(fields: ['username'], message: 'This usernames already exists.')] #[UniqueEntity(fields: ['email'], message: 'This email address is already registered.')] #[ORM\Entity(repositoryClass: UserRepository::class)] diff --git a/src/Filter/OrderedLikeFilter.php b/src/Filter/OrderedLikeFilter.php new file mode 100644 index 0000000..b8840a1 --- /dev/null +++ b/src/Filter/OrderedLikeFilter.php @@ -0,0 +1,66 @@ +isPropertyEnabled($property, $resourceClass) + ) { + return; + } + + $rootAlias = $queryBuilder->getRootAliases()[0]; + $likeParameter = $queryNameGenerator->generateParameterName($property); + $instrParameter = $queryNameGenerator->generateParameterName(sprintf("%s%s", $property, "instr")); + + $queryBuilder + ->andWhere(sprintf("%s.%s LIKE :%s", $rootAlias, $property, $likeParameter)) + ->addOrderBy(sprintf("INSTR(%s.%s, :%s)", $rootAlias, $property, $instrParameter), "ASC") + ->addOrderBy(sprintf("%s.%s", $rootAlias, $property), "ASC") + ->setParameter($likeParameter, sprintf('%%%s%%', $value)) + ->setParameter($instrParameter, $value) + ; + } + + public function getDescription(string $resourceClass): array + { + $description = []; + foreach ($this->properties as $property => $strategy) { + $description["$property"] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_STRING, + 'required' => false, + ]; + } + + return $description; + } +} diff --git a/src/Filter/UserQueryFilter.php b/src/Filter/UserQueryFilter.php new file mode 100644 index 0000000..4be1eaf --- /dev/null +++ b/src/Filter/UserQueryFilter.php @@ -0,0 +1,64 @@ +isPropertyEnabled($property, $resourceClass) + ) { + return; + } + + $rootAlias = $queryBuilder->getRootAliases()[0]; + $parameterName = $queryNameGenerator->generateParameterName($property); + + $queryBuilder + ->andWhere(sprintf('%s.email LIKE :%s', $rootAlias, $parameterName)) + ->orWhere(sprintf('%s.name LIKE :%s', $rootAlias, $parameterName)) + ->orWhere(sprintf('%s.username LIKE :%s', $rootAlias, $parameterName)) + ->setParameter($parameterName, $value); + } + + public function getDescription(string $resourceClass): array + { + $description = []; + foreach ($this->properties as $property => $strategy) { + $description["$property"] = [ + 'property' => $property, + 'type' => Type::BUILTIN_TYPE_STRING, + 'required' => false, + 'description' => 'Query Users by email, name or username. Fuzzy.', + ]; + } + + return $description; + } +} diff --git a/src/Library/Benzina/Pump/ProgressivePumpTrait.php b/src/Library/Benzina/Pump/ProgressivePumpTrait.php new file mode 100644 index 0000000..fb1d87d --- /dev/null +++ b/src/Library/Benzina/Pump/ProgressivePumpTrait.php @@ -0,0 +1,64 @@ +getPumped(Entity::class, $data, ['entityId' => 'dataId']); + * ... + * if ($this->isPumped($record, $alreadyPumped)) continue; + * ``` + */ +trait ProgressivePumpTrait +{ + public function __construct( + private EntityManagerInterface $entityManager + ) { + } + + /** + * Retrieve already pumped data from the pumping data batch + * @param string $entityClass The class to pump the data to + * @param array $pumpingBatch The pumping data batch + * @param array $matchCriteria An array with the matching criteria between the entity key and the data key + */ + public function getPumped( + string $entityClass, + array $pumpingBatch, + array $matchCriteria + ): array { + $repository = $this->entityManager->getRepository($entityClass); + + $entityKey = \array_keys($matchCriteria)[0]; + $pumpingKey = $matchCriteria[$entityKey]; + + $pumped = $repository->findBy([$entityKey => \array_map(function ($data) use ($pumpingKey) { + return $data[$pumpingKey]; + }, $pumpingBatch)]); + + $pumpedByKey = []; + foreach ($pumped as $pumped) { + $getter = sprintf("get%s", ucfirst($entityKey)); + $pumpedByKey[$pumped->$getter()] = $pumped; + } + + return $pumpedByKey; + } + + /** + * Determine if a pumping data record is already in a pumped data batch + * @param array $pumpingRecord The data record to be pumped + * @param array $pumpedBatch The pumped data batch + */ + public function isPumped(array $pumpingRecord, array $pumpedBatch): bool + { + return \array_key_exists($pumpingRecord['id'], $pumpedBatch); + } +} diff --git a/src/Library/Benzina/Pump/UsersPump.php b/src/Library/Benzina/Pump/UsersPump.php index bca058f..2d84230 100644 --- a/src/Library/Benzina/Pump/UsersPump.php +++ b/src/Library/Benzina/Pump/UsersPump.php @@ -7,6 +7,46 @@ use Doctrine\ORM\EntityManagerInterface; class UsersPump implements PumpInterface { + use ArrayPumpTrait; + use ProgressivePumpTrait; + + private const USER_KEYS = [ + 'id', + 'name', + 'location', + 'email', + 'password', + 'gender', + 'birthyear', + 'entity_type', + 'legal_entity', + 'origin_register', + 'about', + 'keywords', + 'active', + 'avatar', + 'contribution', + 'twitter', + 'facebook', + 'instagram', + 'identica', + 'linkedin', + 'amount', + 'num_patron', + 'num_patron_active', + 'worth', + 'created', + 'modified', + 'token', + 'rememberme', + 'hide', + 'confirmed', + 'lang', + 'node', + 'num_invested', + 'num_owned', + ]; + public function __construct( private EntityManagerInterface $entityManager ) { @@ -18,30 +58,27 @@ class UsersPump implements PumpInterface return false; } - if (!\array_key_exists('email', $data[0]) && !\array_key_exists('password', $data[0])) { - return false; - } - - return true; + return $this->hasAllKeys($data[0], self::USER_KEYS); } public function process(mixed $data): void { - foreach ($data as $key => $userData) { - $username = $this->normalizeUsername($userData['id']); - if (!$username) { - $username = $this->normalizeUsername($userData['email']); + $pumped = $this->getPumped(User::class, $data, ['migratedReference' => 'id']); + + foreach ($data as $key => $record) { + if ($this->isPumped($record, $pumped)) { + continue; } $user = new User; - $user->setUsername($username); - $user->setPassword($userData['password'] ?? ""); - $user->setEmail($userData['email']); - $user->setName($userData['name']); + $user->setUsername($this->getUsername($record)); + $user->setPassword($record['password'] ?? ""); + $user->setEmail($record['email']); + $user->setName($record['name']); $user->setActive(false); $user->setConfirmed(false); $user->setMigrated(true); - $user->setMigratedReference($userData['id']); + $user->setMigratedReference($record['id']); $this->entityManager->persist($user); } @@ -67,4 +104,15 @@ class UsersPump implements PumpInterface return $username; } + + private function getUsername(array $data): string + { + $username = $this->normalizeUsername($data['id']); + + if (!$username) { + $username = $this->normalizeUsername($data['email']); + } + + return $username; + } }