From d6981eaa28ca3e6c05d64782c8ba4bb602c7c51a Mon Sep 17 00:00:00 2001 From: Kilderson Sena Date: Sun, 26 Sep 2021 14:40:13 -0300 Subject: [PATCH 1/5] base classes configuration --- src/Shared/Adapter/Dto.php | 25 +++ src/Shared/Adapter/DtoBase.php | 100 ++++++++++ .../Adapter/Expcetion/InvalidDtoParam.php | 44 +++++ .../Domain/Contracts/DomainException.php | 10 + src/Shared/Domain/Contracts/Entity.php | 43 ++++ src/Shared/Domain/Contracts/ValueObject.php | 33 ++++ src/Shared/Domain/EntityBase.php | 184 ++++++++++++++++++ .../Domain/Exceptions/EntityException.php | 46 +++++ .../Domain/Exceptions/InvalidUrlException.php | 38 ++++ src/Shared/Domain/ValueObjectBase.php | 46 +++++ src/Shared/Domain/ValueObjects/Url.php | 34 ++++ 11 files changed, 603 insertions(+) create mode 100644 src/Shared/Adapter/Dto.php create mode 100644 src/Shared/Adapter/DtoBase.php create mode 100644 src/Shared/Adapter/Expcetion/InvalidDtoParam.php create mode 100644 src/Shared/Domain/Contracts/DomainException.php create mode 100644 src/Shared/Domain/Contracts/Entity.php create mode 100644 src/Shared/Domain/Contracts/ValueObject.php create mode 100644 src/Shared/Domain/EntityBase.php create mode 100644 src/Shared/Domain/Exceptions/EntityException.php create mode 100644 src/Shared/Domain/Exceptions/InvalidUrlException.php create mode 100644 src/Shared/Domain/ValueObjectBase.php create mode 100644 src/Shared/Domain/ValueObjects/Url.php diff --git a/src/Shared/Adapter/Dto.php b/src/Shared/Adapter/Dto.php new file mode 100644 index 0000000..1a07dcb --- /dev/null +++ b/src/Shared/Adapter/Dto.php @@ -0,0 +1,25 @@ + + */ +interface Dto +{ + /** + * Associative array such as `'property' => 'value'` with all boundary values + * @return array + */ + public function values(): array; + + /** + * Get a boundary value by property + * @param string $property + * @return mixed + */ + public function get(string $property); +} diff --git a/src/Shared/Adapter/DtoBase.php b/src/Shared/Adapter/DtoBase.php new file mode 100644 index 0000000..826f0af --- /dev/null +++ b/src/Shared/Adapter/DtoBase.php @@ -0,0 +1,100 @@ + + */ +abstract class DtoBase implements Dto +{ + private array $boundaryValues = []; + + /** + * Boundary constructor. + * @param array $values + * @throws InvalidDtoParam + */ + final private function __construct(array $values) + { + foreach ($values as $key => $value) { + if (mb_strstr($key, '_') !== false) { + $key = lcfirst(str_replace('_', '', ucwords($key, '_'))); + } + + if (!property_exists($this, $key)) { + throw InvalidDtoParam::forDynamicParam(get_class(), $key); + } + + $this->{$key} = $value; + $this->boundaryValues[$key] = $this->get($key); + } + } + + /** + * Static method to create a Boundary (Input or Output) + * @param array $values Associative array such as `'property' => 'value'` + * @throws InvalidDtoParam + */ + public static function create(array $values): static + { + return new static($values); + } + + /** + * {@inheritdoc} + */ + public function values(): array + { + return $this->boundaryValues; + } + + /** + * {@inheritdoc} + * @throws InvalidDtoParam + */ + public function get(string $property) + { + return $this->__get($property); + } + + /** + * Magic getter method to get a Boundary property value + * @param string $name + * @return mixed + * @throws InvalidDtoParam + */ + public function __get(string $name) + { + $getter = "get" . ucfirst($name); + + if (method_exists($this, $getter)) { + return $this->{$getter}(); + } + + if (!property_exists($this, $name)) { + throw InvalidDtoParam::forDynamicParam(get_class(), $name); + } + + return $this->{$name}; + } + + /** + * @param string $name + * @param mixed $value + * @throws InvalidDtoParam + */ + public function __set(string $name, mixed $value) + { + throw InvalidDtoParam::forReadonlyProperty(get_class(), $name, $value); + } + + public function __isset($name): bool + { + return property_exists($this, $name); + } +} diff --git a/src/Shared/Adapter/Expcetion/InvalidDtoParam.php b/src/Shared/Adapter/Expcetion/InvalidDtoParam.php new file mode 100644 index 0000000..41839eb --- /dev/null +++ b/src/Shared/Adapter/Expcetion/InvalidDtoParam.php @@ -0,0 +1,44 @@ +details = $details; + } + + public static function forDynamicParam(string $className, string $property): self + { + return new self(sprintf( + "It couldn't construct DTO '%s' because the property '%s' doesn't exist", + $className, $property + ), [ + 'className' => $className, + 'property' => $property, + ]); + } + + public static function forReadonlyProperty(string $className, string $property, $value): self + { + return new self(sprintf( + "You cannot change the property '%s' of the DTO class '%s' because it is read-only.", + $property, $className + ), [ + 'className' => $className, + 'property' => $property, + 'value' => $value + ]); + } +} \ No newline at end of file diff --git a/src/Shared/Domain/Contracts/DomainException.php b/src/Shared/Domain/Contracts/DomainException.php new file mode 100644 index 0000000..b86bf01 --- /dev/null +++ b/src/Shared/Domain/Contracts/DomainException.php @@ -0,0 +1,10 @@ + 'value'` + * @return void + */ + public function fill(array $values): void; + + /** + * Method that contains the property setter logic + * @param string $property Object property name + * @param mixed $value Value to be inserted in property + * @return Entity + */ + public function set(string $property, $value): Entity; + + /** + * Method that contains the property getter logic + * @param string $property Object property name + * @return mixed + */ + public function get(string $property); + + /** + * Output an array based on entity properties + * @param bool $toSnakeCase + * @return array + */ + public function toArray(bool $toSnakeCase = false): array; +} diff --git a/src/Shared/Domain/Contracts/ValueObject.php b/src/Shared/Domain/Contracts/ValueObject.php new file mode 100644 index 0000000..c947ce9 --- /dev/null +++ b/src/Shared/Domain/Contracts/ValueObject.php @@ -0,0 +1,33 @@ + + * + * @property-read int|string $id + */ +abstract class EntityBase implements Entity +{ + /** + * @var string|int + */ + protected $id; + + /** + * Entity constructor. + * @param array $values + * @throws EntityException + */ + final private function __construct(array $values) + { + $this->fill($values); + } + + /** + * Static method to create an Entity + * @param array $values + * @return EntityBase + * @throws EntityException + */ + public static function create(array $values): self + { + return new static($values); + } + + /** + * @inheritDoc + */ + public function getId() + { + return $this->id; + } + + /** + * @inheritDoc + * @throws EntityException + */ + public function fill(array $values): void + { + foreach ($values as $attribute => $value) { + $this->set($attribute, $value); + } + } + + /** + * @inheritDoc + * @throws EntityException + */ + public function set(string $property, $value): Entity + { + if (mb_strstr($property, '_') !== false) { + $property = lcfirst(str_replace('_', '', ucwords($property, '_'))); + } + + $setter = 'set' . str_replace('_', '', ucwords($property, '_')); + + if (method_exists($this, $setter)) { + $this->{$setter}($value); + return $this; + } + + if (!property_exists($this, $property)) { + $className = get_class(); + throw EntityException::readonlyProperty($className, $property, [ + 'className' => $className, + 'property' => $property, + 'value' => $value + ]); + } + + $this->{$property} = $value; + return $this; + } + + /** + * @inheritDoc + */ + public function get(string $property) + { + return $this->{$property}; + } + + /** + * @inheritDoc + * @throws ReflectionException + */ + public function toArray(bool $toSnakeCase = false): array + { + $props = []; + $propertyList = get_object_vars($this); + + /** @var int|string|object $value */ + foreach ($propertyList as $prop => $value) { + if ($value instanceof DateTimeInterface) { + $propertyList[$prop] = $value->format(DATE_ATOM); + continue; + } + + if (is_object($value)) { + $reflectObject = new ReflectionClass(get_class($value)); + $properties = $reflectObject->getProperties(); + $propertyList[$prop] = []; + + foreach ($properties as $property) { + $property->setAccessible(true); + $propertyList[$prop][$property->getName()] = $property->getValue($value); + } + } + } + + $propertyList = json_decode(json_encode($propertyList), true); + + foreach ($propertyList as $name => $value) { + if ($toSnakeCase) { + $name = mb_strtolower(preg_replace('/(?{$getter}(); + } + + if (!property_exists($this, $name)) { + $className = get_class(); + throw EntityException::propertyDoesNotExists($className, $name, [ + 'className' => $className, + 'propertyName' => $name + ]); + } + + return $this->{$name}; + } + + /** + * @param mixed $value + * @throws EntityException + */ + public function __set(string $name, $value) + { + $className = get_class(); + throw EntityException::readonlyProperty($className, $name, [ + 'className' => $className, + 'property' => $name, + 'value' => $value + ]); + } +} diff --git a/src/Shared/Domain/Exceptions/EntityException.php b/src/Shared/Domain/Exceptions/EntityException.php new file mode 100644 index 0000000..cc821ed --- /dev/null +++ b/src/Shared/Domain/Exceptions/EntityException.php @@ -0,0 +1,46 @@ +message = $message; + $this->details = $details; + parent::__construct($this->message, $code, $previous); + } + + public static function readonlyProperty(string $className, string $propertyName, array $details = []): self + { + return new self( + "You cannot change the property '{$propertyName}' of the Entity class '{$className}' because it is read-only.", + $details + ); + } + + public static function propertyDoesNotExists(string $className, string $propertyName, array $details = []): self + { + return new self( + "You cannot get the property '{$propertyName}' because it doesn't exist in Entity '{$className}'", + $details + ); + } + + public function details(): array + { + return $this->details; + } +} diff --git a/src/Shared/Domain/Exceptions/InvalidUrlException.php b/src/Shared/Domain/Exceptions/InvalidUrlException.php new file mode 100644 index 0000000..1a3621e --- /dev/null +++ b/src/Shared/Domain/Exceptions/InvalidUrlException.php @@ -0,0 +1,38 @@ +message = $message; + $this->details = $details; + parent::__construct($this->message, $code, $previous); + } + + public static function forEmptyUrl(): self + { + return new self("URL cannot be empty."); + } + + public static function forInvalidUrl(string $giveUrl, array $details = []): self + { + return new self("The given URL '{$giveUrl}' is invalid", $details); + } + + public function details(): array + { + return $this->details(); + } +} \ No newline at end of file diff --git a/src/Shared/Domain/ValueObjectBase.php b/src/Shared/Domain/ValueObjectBase.php new file mode 100644 index 0000000..0238347 --- /dev/null +++ b/src/Shared/Domain/ValueObjectBase.php @@ -0,0 +1,46 @@ +value(); + } + + /** + * @inheritDoc + */ + public function isEqualsTo(ValueObject $valueObject): bool + { + return $this->objectHash() === $valueObject->objectHash(); + } + + /** + * @inheritDoc + * @throws ReflectionException + */ + public function objectHash(): string + { + $reflectObject = new ReflectionClass(get_class($this)); + $props = $reflectObject->getProperties(); + $value = ''; + + foreach ($props as $prop) { + $prop->setAccessible(true); + $value .= $prop->getValue($this); + } + + return md5($value); + } +} diff --git a/src/Shared/Domain/ValueObjects/Url.php b/src/Shared/Domain/ValueObjects/Url.php new file mode 100644 index 0000000..dc5fd54 --- /dev/null +++ b/src/Shared/Domain/ValueObjects/Url.php @@ -0,0 +1,34 @@ + $url]); + } + + $this->url = $url; + } + + public function value(): mixed + { + return $this->url; + } +} From e41b706f58912be5e5281c801fb8ab2c1a49f7cc Mon Sep 17 00:00:00 2001 From: Kilderson Sena Date: Sun, 26 Sep 2021 14:40:41 -0300 Subject: [PATCH 2/5] creation of domain stuff --- src/Domain/Entity/LongUrl.php | 79 ++++++++++++++++++++ src/Domain/Exceptions/InvalidLongUrl.php | 32 ++++++++ src/Domain/Repository/LongUrlRepository.php | 12 +++ src/Domain/UseCase/ShortenUrl/InputData.php | 19 +++++ src/Domain/UseCase/ShortenUrl/OutputData.php | 21 ++++++ src/Domain/UseCase/ShortenUrl/ShortenUrl.php | 34 +++++++++ src/Domain/ValueObject/LongUrlType.php | 33 ++++++++ 7 files changed, 230 insertions(+) create mode 100644 src/Domain/Entity/LongUrl.php create mode 100644 src/Domain/Exceptions/InvalidLongUrl.php create mode 100644 src/Domain/Repository/LongUrlRepository.php create mode 100644 src/Domain/UseCase/ShortenUrl/InputData.php create mode 100644 src/Domain/UseCase/ShortenUrl/OutputData.php create mode 100644 src/Domain/UseCase/ShortenUrl/ShortenUrl.php create mode 100644 src/Domain/ValueObject/LongUrlType.php diff --git a/src/Domain/Entity/LongUrl.php b/src/Domain/Entity/LongUrl.php new file mode 100644 index 0000000..076d0fd --- /dev/null +++ b/src/Domain/Entity/LongUrl.php @@ -0,0 +1,79 @@ +baseUrlToShortUrl = new Url($baseUrl); + } + + protected function setLongUrl(string $longUrl) + { + $this->longUrl = new Url($longUrl); + } + + protected function setShortUrl(string $shortUrl) + { + $this->shortUrl = new Url($shortUrl); + } + + protected function setType(string $type) + { + $this->type = new LongUrlType($type); + } + + protected function setCreatedAt(string $createdAt) + { + if (empty($createdAt)) { + return null; + } + + $this->createdAt = new DateTimeImmutable($createdAt); + } + + public function calculateEconomyRate(): float + { + return ceil(100 - ((strlen($this->shortUrl->value()) * 100) / strlen($this->longUrl->value()))); + } + + public function getShortUrlPath(): string + { + $urlParts = explode('/', $this->shortUrl->value()); + return end($urlParts); + } + + public static function generatePathToShortUrl(): string + { + return substr(sha1(uniqid((string)rand(), true)), 0, 5); + } +} diff --git a/src/Domain/Exceptions/InvalidLongUrl.php b/src/Domain/Exceptions/InvalidLongUrl.php new file mode 100644 index 0000000..5375dd9 --- /dev/null +++ b/src/Domain/Exceptions/InvalidLongUrl.php @@ -0,0 +1,32 @@ +message = $message; + $this->details = $details; + parent::__construct($this->message, $code, $previous); + } + + public static function forInvalidType(string $giveType, array $details = []): self + { + return new self("The given URL Type '{$giveType}' is invalid", $details); + } + + public function details(): array + { + return $this->details(); + } +} \ No newline at end of file diff --git a/src/Domain/Repository/LongUrlRepository.php b/src/Domain/Repository/LongUrlRepository.php new file mode 100644 index 0000000..891d4f0 --- /dev/null +++ b/src/Domain/Repository/LongUrlRepository.php @@ -0,0 +1,12 @@ + $input->longUrl, + 'baseUrlToShortUrl' => $input->baseUrl, + 'type' => $input->type + ]); + + $longUrl = $this->urlRepo->shortLongUrl($longUrl); + + return OutputData::create([ + 'longUrl' => $longUrl->longUrl, + 'shortenedUrl' => $longUrl->shortUrl, + 'economyRate' => $longUrl->calculateEconomyRate(), + 'createdAt' => $longUrl->createdAt->format(DateTimeInterface::ATOM), + ]); + } +} \ No newline at end of file diff --git a/src/Domain/ValueObject/LongUrlType.php b/src/Domain/ValueObject/LongUrlType.php new file mode 100644 index 0000000..2fc6c9d --- /dev/null +++ b/src/Domain/ValueObject/LongUrlType.php @@ -0,0 +1,33 @@ +type = $type; + } + + public function value(): mixed + { + return $this->type; + } +} \ No newline at end of file From 3381cace175dd02d18352085ee184ca001293376 Mon Sep 17 00:00:00 2001 From: Kilderson Sena Date: Sun, 26 Sep 2021 16:47:55 -0300 Subject: [PATCH 3/5] implementation of adapters and infra stuff --- config/dependencies.php | 15 +++ config/middleware.php | 2 +- config/routes.php | 6 +- .../Controllers/AccessUrlController.php | 2 +- .../Controllers/HomeController.php | 2 +- .../Controllers/ShortenUrlController.php | 72 +++++++++++ .../Middleware/SessionMiddleware.php | 2 +- .../Middleware/SlimFlashMiddleware.php | 2 +- .../Database/DbLongUrlRepository.php | 46 +++++++ src/Controllers/ShortenUrlController.php | 119 ------------------ src/Domain/Entity/LongUrl.php | 16 ++- src/Domain/UseCase/ShortenUrl/ShortenUrl.php | 4 +- src/Shared/Adapter/Contracts/DatabaseOrm.php | 15 +++ src/Shared/Adapter/{ => Contracts}/Dto.php | 2 +- .../Adapter/Contracts/UuidGenerator.php | 11 ++ src/Shared/Adapter/DtoBase.php | 7 +- src/Shared/Infra/PdoOrm.php | 57 +++++++++ src/Shared/Infra/RamseyUiidAdapter.php | 21 ++++ 18 files changed, 262 insertions(+), 139 deletions(-) rename src/{ => Adapter}/Controllers/AccessUrlController.php (98%) rename src/{ => Adapter}/Controllers/HomeController.php (96%) create mode 100644 src/Adapter/Controllers/ShortenUrlController.php rename src/{ => Adapter}/Middleware/SessionMiddleware.php (95%) rename src/{ => Adapter}/Middleware/SlimFlashMiddleware.php (95%) create mode 100644 src/Adapter/Repository/Database/DbLongUrlRepository.php delete mode 100644 src/Controllers/ShortenUrlController.php create mode 100644 src/Shared/Adapter/Contracts/DatabaseOrm.php rename src/Shared/Adapter/{ => Contracts}/Dto.php (91%) create mode 100644 src/Shared/Adapter/Contracts/UuidGenerator.php create mode 100644 src/Shared/Infra/PdoOrm.php create mode 100644 src/Shared/Infra/RamseyUiidAdapter.php diff --git a/config/dependencies.php b/config/dependencies.php index 67e5055..53f11a8 100644 --- a/config/dependencies.php +++ b/config/dependencies.php @@ -2,6 +2,12 @@ declare(strict_types=1); +use App\Adapter\Repository\Database\DbLongUrlRepository; +use App\Domain\Repository\LongUrlRepository; +use App\Shared\Adapter\Contracts\DatabaseOrm; +use App\Shared\Adapter\Contracts\UuidGenerator; +use App\Shared\Infra\PdoOrm; +use App\Shared\Infra\RamseyUiidAdapter; use DI\ContainerBuilder; use Odan\Session\Middleware\SessionMiddleware; use Odan\Session\PhpSession; @@ -24,5 +30,14 @@ $storage = []; return new Messages($storage); }, + DatabaseOrm::class => function (ContainerInterface $c) { + return new PdoOrm($c->get('db')); + }, + UuidGenerator::class => function (ContainerInterface $c) { + return new RamseyUiidAdapter(); + }, + LongUrlRepository::class => function (ContainerInterface $c) { + return new DbLongUrlRepository($c->get(DatabaseOrm::class), $c->get(RamseyUiidAdapter::class)); + }, ]); }; diff --git a/config/middleware.php b/config/middleware.php index b690f54..41fdda0 100644 --- a/config/middleware.php +++ b/config/middleware.php @@ -3,7 +3,7 @@ declare(strict_types=1); use Slim\App; -use App\Middleware\SessionMiddleware; +use App\Adapter\Middleware\SessionMiddleware; use Slim\Views\TwigMiddleware; return function (App $app) { diff --git a/config/routes.php b/config/routes.php index b2607d5..6eb4a5f 100644 --- a/config/routes.php +++ b/config/routes.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use App\Controllers\AccessUrlController; -use App\Controllers\HomeController; -use App\Controllers\ShortenUrlController; +use App\Adapter\Controllers\AccessUrlController; +use App\Adapter\Controllers\HomeController; +use App\Adapter\Controllers\ShortenUrlController; use Slim\App; use Slim\Routing\RouteCollectorProxy; diff --git a/src/Controllers/AccessUrlController.php b/src/Adapter/Controllers/AccessUrlController.php similarity index 98% rename from src/Controllers/AccessUrlController.php rename to src/Adapter/Controllers/AccessUrlController.php index 94054e1..d5f7b06 100644 --- a/src/Controllers/AccessUrlController.php +++ b/src/Adapter/Controllers/AccessUrlController.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Controllers; +namespace App\Adapter\Controllers; use DateTimeImmutable; use PDO; diff --git a/src/Controllers/HomeController.php b/src/Adapter/Controllers/HomeController.php similarity index 96% rename from src/Controllers/HomeController.php rename to src/Adapter/Controllers/HomeController.php index eb0d737..2768cc2 100644 --- a/src/Controllers/HomeController.php +++ b/src/Adapter/Controllers/HomeController.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Controllers; +namespace App\Adapter\Controllers; use PDO; use Psr\Container\ContainerInterface; diff --git a/src/Adapter/Controllers/ShortenUrlController.php b/src/Adapter/Controllers/ShortenUrlController.php new file mode 100644 index 0000000..07406f5 --- /dev/null +++ b/src/Adapter/Controllers/ShortenUrlController.php @@ -0,0 +1,72 @@ +config = $container->get('config'); + $this->useCase = $container->get(ShortenUrl::class); + } + + public function __invoke(Request $request, Response $response, array $args): Response + { + $contents = $request->getParsedBody(); + + if (empty($contents) || !array_key_exists('long_url', $contents)) { + $newResponse = $response + ->withHeader('Content-type', 'application/json') + ->withStatus(400); + + $newResponse->getBody()->write(json_encode([ + 'status' => 'fail', + 'data' => ['long_url' => 'missing-param'] + ])); + + return $newResponse; + } + + if (empty($contents['long_url'])) { + $newResponse = $response + ->withHeader('Content-type', 'application/json') + ->withStatus(400); + + $newResponse->getBody()->write(json_encode([ + 'status' => 'fail', + 'data' => ['long_url' => 'empty-value'] + ])); + + return $newResponse; + } + + $result = $this->useCase->execute(InputData::create([ + 'longUrl' => $contents['long_url'], + 'type' => LongUrlType::TYPE_RANDOM, + 'baseUrl' => $this->config['baseUrl'], + ])); + + $newResponse = $response + ->withHeader('Content-type', 'application/json') + ->withStatus(200); + + $newResponse->getBody()->write(json_encode([ + 'status' => 'success', + 'data' => $result->values() + ])); + + return $newResponse; + } +} \ No newline at end of file diff --git a/src/Middleware/SessionMiddleware.php b/src/Adapter/Middleware/SessionMiddleware.php similarity index 95% rename from src/Middleware/SessionMiddleware.php rename to src/Adapter/Middleware/SessionMiddleware.php index 632de19..f6827e9 100644 --- a/src/Middleware/SessionMiddleware.php +++ b/src/Adapter/Middleware/SessionMiddleware.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Middleware; +namespace App\Adapter\Middleware; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; diff --git a/src/Middleware/SlimFlashMiddleware.php b/src/Adapter/Middleware/SlimFlashMiddleware.php similarity index 95% rename from src/Middleware/SlimFlashMiddleware.php rename to src/Adapter/Middleware/SlimFlashMiddleware.php index 8343f7c..87f2c82 100644 --- a/src/Middleware/SlimFlashMiddleware.php +++ b/src/Adapter/Middleware/SlimFlashMiddleware.php @@ -1,6 +1,6 @@ orm->read('urls', ['short_url_path' => $url->getShortUrlPath()]); + + if (!is_null($urlRecord)) { + $url->renewShortUrlPath(); + continue; + } + + break; + } + + $url->set('id', $this->uuidGenerator->create()); + + $this->orm->create('urls', [ + 'id' => $this->uuidGenerator->toBytes($url->id), + 'uuid' => $url->id, + 'long_url' => $url->longUrl->value(), + 'short_url_path' => $url->getShortUrlPath(), + 'type' => $url->type->value(), + 'economy_rate' => $url->calculateEconomyRate(), + 'created_at' => $url->createdAt->format('Y-m-d H:i:s') + ]); + + return $url; + } +} \ No newline at end of file diff --git a/src/Controllers/ShortenUrlController.php b/src/Controllers/ShortenUrlController.php deleted file mode 100644 index f7d27bb..0000000 --- a/src/Controllers/ShortenUrlController.php +++ /dev/null @@ -1,119 +0,0 @@ -db = $container->get('db'); - $this->config = $container->get('config'); - } - - public function __invoke(Request $request, Response $response, array $args): Response - { - $contents = $request->getParsedBody(); - - if (empty($contents) || !array_key_exists('huge_url', $contents)) { - $newResponse = $response - ->withHeader('Content-type', 'application/json') - ->withStatus(400); - - $newResponse->getBody()->write(json_encode([ - 'status' => 'fail', - 'data' => ['huge_url' => 'missing-param'] - ])); - - return $newResponse; - } - - if (empty($contents['huge_url'])) { - $newResponse = $response - ->withHeader('Content-type', 'application/json') - ->withStatus(400); - - $newResponse->getBody()->write(json_encode([ - 'status' => 'fail', - 'data' => ['huge_url' => 'empty-value'] - ])); - - return $newResponse; - } - - if (filter_var($contents['huge_url'], FILTER_VALIDATE_URL) === false) { - $newResponse = $response - ->withHeader('Content-type', 'application/json') - ->withStatus(400); - - $newResponse->getBody()->write(json_encode([ - 'status' => 'fail', - 'data' => ['huge_url' => 'invalid-url'] - ])); - - return $newResponse; - } - - $stmt = $this->db->prepare(trim(" - INSERT INTO `urls` (`id`, `uuid`, `long_url`, `short_url_path`, `economy_rate`, `created_at`) - VALUES (:id, :uuid, :long_url, :short_url_path, :economy_rate, :created_at) - ")); - - $shortUrlPath = substr(sha1(uniqid((string)rand(), true)), 0, 5); - - while (true) { - $stmt2 = $this->db->prepare('select `id` from `urls` where `short_url_path` = :path'); - $stmt2->execute(['path' => $shortUrlPath]); - $row = $stmt2->fetch(); - - if ($row !== false) { - $shortUrlPath = substr(sha1(uniqid((string)rand(), true)), 0, 5); - continue; - } - - break; - } - - $fullShortenUrl = $this->config['baseUrl'] . '/' . $shortUrlPath; - $uuid = Uuid::uuid4(); - $createdAt = new DateTimeImmutable(); - $economyRate = ceil(100 - ((strlen($fullShortenUrl) * 100) / strlen($contents['huge_url']))); - - $stmt->execute([ - 'id' => $uuid->getBytes(), - 'uuid' => $uuid->toString(), - 'long_url' => $contents['huge_url'], - 'short_url_path' => $shortUrlPath, - 'economy_rate' => $economyRate, - 'created_at' => $createdAt->format('Y-m-d H:i:s') - ]); - - $newResponse = $response - ->withHeader('Content-type', 'application/json') - ->withStatus(200); - - $newResponse->getBody()->write(json_encode([ - 'status' => 'success', - 'data' => [ - 'huge' => $contents['huge_url'], - 'shortened' => $fullShortenUrl, - 'created_at' => $createdAt->format(DateTimeInterface::ATOM), - 'economyRate' => $economyRate - ] - ])); - - return $newResponse; - } -} \ No newline at end of file diff --git a/src/Domain/Entity/LongUrl.php b/src/Domain/Entity/LongUrl.php index 076d0fd..0c39295 100644 --- a/src/Domain/Entity/LongUrl.php +++ b/src/Domain/Entity/LongUrl.php @@ -15,7 +15,6 @@ * @property-read Url $longUrl * @property-read Url $shortUrl * @property-read LongUrlType $type - * @property-read float $economyRate * @property-read DateTimeInterface $createdAt */ final class LongUrl extends EntityBase @@ -24,10 +23,14 @@ final class LongUrl extends EntityBase protected Url $longUrl; protected Url $shortUrl; protected LongUrlType $type; - protected DateTimeInterface $createdAt; + protected ?DateTimeInterface $createdAt; public static function create(array $values): EntityBase { + if (!isset($values['createdAt'])) { + $values['createdAt'] = (new DateTimeImmutable())->format(DateTimeInterface::ATOM); + } + $values['shortUrl'] = $values['baseUrlToShortUrl'] . '/' . self::generatePathToShortUrl(); return parent::create($values); } @@ -54,10 +57,6 @@ protected function setType(string $type) protected function setCreatedAt(string $createdAt) { - if (empty($createdAt)) { - return null; - } - $this->createdAt = new DateTimeImmutable($createdAt); } @@ -72,6 +71,11 @@ public function getShortUrlPath(): string return end($urlParts); } + public function renewShortUrlPath(): void + { + $this->shortUrl = new Url($this->baseUrlToShortUrl . '/' . self::generatePathToShortUrl()); + } + public static function generatePathToShortUrl(): string { return substr(sha1(uniqid((string)rand(), true)), 0, 5); diff --git a/src/Domain/UseCase/ShortenUrl/ShortenUrl.php b/src/Domain/UseCase/ShortenUrl/ShortenUrl.php index 43c9cfb..fb5e889 100644 --- a/src/Domain/UseCase/ShortenUrl/ShortenUrl.php +++ b/src/Domain/UseCase/ShortenUrl/ShortenUrl.php @@ -25,8 +25,8 @@ public function execute(InputData $input): OutputData $longUrl = $this->urlRepo->shortLongUrl($longUrl); return OutputData::create([ - 'longUrl' => $longUrl->longUrl, - 'shortenedUrl' => $longUrl->shortUrl, + 'longUrl' => $longUrl->longUrl->value(), + 'shortenedUrl' => $longUrl->shortUrl->value(), 'economyRate' => $longUrl->calculateEconomyRate(), 'createdAt' => $longUrl->createdAt->format(DateTimeInterface::ATOM), ]); diff --git a/src/Shared/Adapter/Contracts/DatabaseOrm.php b/src/Shared/Adapter/Contracts/DatabaseOrm.php new file mode 100644 index 0000000..ec53df5 --- /dev/null +++ b/src/Shared/Adapter/Contracts/DatabaseOrm.php @@ -0,0 +1,15 @@ +{$key} = $value; - $this->boundaryValues[$key] = $this->get($key); + $this->values[$key] = $this->get($key); } } @@ -50,7 +51,7 @@ public static function create(array $values): static */ public function values(): array { - return $this->boundaryValues; + return $this->values; } /** diff --git a/src/Shared/Infra/PdoOrm.php b/src/Shared/Infra/PdoOrm.php new file mode 100644 index 0000000..a335ecd --- /dev/null +++ b/src/Shared/Infra/PdoOrm.php @@ -0,0 +1,57 @@ + ':' . $columnName, $columns); + $columns = array_map(fn($columnName) => '`' . $columnName . '`', $columns); + $sql = sprintf( + 'INSERT INTO `%s` (%s) VALUES (%s)', + $tableName, + implode(',', $columns), + implode(',', $columnsVars) + ); + $stmt = $this->pdo->prepare($sql); + $stmt->execute($values); + + return $this->pdo->lastInsertId('uuid'); + } + + public function read(string $tableName, array $filters, array $options = []): ?array + { + return null; + } + + public function update(string $tableName, array $values, array $conditions): bool + { + return true; + } + + public function delete(string $tableName, array $conditions): bool + { + return true; + } + + public function search(string $tableName, array $filters, array $options = []): array + { + return []; + } + + public function persist(string $tableName, array $values): int|string + { + return ''; + } +} \ No newline at end of file diff --git a/src/Shared/Infra/RamseyUiidAdapter.php b/src/Shared/Infra/RamseyUiidAdapter.php new file mode 100644 index 0000000..b29f6cc --- /dev/null +++ b/src/Shared/Infra/RamseyUiidAdapter.php @@ -0,0 +1,21 @@ +toString(); + } + + public function toBytes(string $uuid): string + { + return Uuid::fromString($uuid)->getBytes(); + } +} \ No newline at end of file From aca1ba0b7529f7bd46e5eb812012d5cf75ce9809 Mon Sep 17 00:00:00 2001 From: Kilderson Sena Date: Sun, 26 Sep 2021 18:01:32 -0300 Subject: [PATCH 4/5] access url controller refactoring --- config/dependencies.php | 6 ++- .../Controllers/AccessUrlController.php | 43 ++++++------------- .../Database/DbLongUrlRepository.php | 39 ++++++++++++++++- src/Domain/Entity/LongUrl.php | 7 ++- src/Domain/Repository/LongUrlRepository.php | 2 + src/Shared/Domain/EntityBase.php | 5 +-- src/Shared/Infra/PdoOrm.php | 34 ++++++++++++++- 7 files changed, 97 insertions(+), 39 deletions(-) diff --git a/config/dependencies.php b/config/dependencies.php index 53f11a8..99f6564 100644 --- a/config/dependencies.php +++ b/config/dependencies.php @@ -37,7 +37,11 @@ return new RamseyUiidAdapter(); }, LongUrlRepository::class => function (ContainerInterface $c) { - return new DbLongUrlRepository($c->get(DatabaseOrm::class), $c->get(RamseyUiidAdapter::class)); + return new DbLongUrlRepository( + $c->get(DatabaseOrm::class), + $c->get(RamseyUiidAdapter::class), + $c->get('config') + ); }, ]); }; diff --git a/src/Adapter/Controllers/AccessUrlController.php b/src/Adapter/Controllers/AccessUrlController.php index d5f7b06..d10482f 100644 --- a/src/Adapter/Controllers/AccessUrlController.php +++ b/src/Adapter/Controllers/AccessUrlController.php @@ -4,59 +4,44 @@ namespace App\Adapter\Controllers; -use DateTimeImmutable; -use PDO; +use App\Domain\Repository\LongUrlRepository; use Psr\Container\ContainerInterface; use Psr\Http\Message\RequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; -use Ramsey\Uuid\Uuid; use Slim\Views\Twig; final class AccessUrlController { - private PDO $db; private Twig $view; + private LongUrlRepository $longUrlRepo; public function __construct(ContainerInterface $container) { - $this->db = $container->get('db'); $this->view = $container->get('view'); + $this->longUrlRepo = $container->get(LongUrlRepository::class); } public function __invoke(Request $request, Response $response, array $args): Response { - $stmt = $this->db->prepare("SELECT `id`, `long_url` FROM `urls` WHERE `short_url_path` = :path"); - $stmt->execute(['path' => $args['path']]); - $row = $stmt->fetch(); - - if (!$row) { + $url = $this->longUrlRepo->getUrlByPath($args['path']); + + if (is_null($url)) { return $this->view->render($response, 'notfound.html.twig', []); } - $sql = "INSERT INTO `urls_logs` (`id`, `uuid`, `url_id`, `created_at`, `meta`) VALUES (:id, :uuid, :url_id, :created_at, :meta)"; - $stmt = $this->db->prepare($sql); - - $uuid = Uuid::uuid4(); - - $stmt->execute([ - 'id' => $uuid->getBytes(), - 'uuid' => $uuid->toString(), - 'url_id' => $row['id'], - 'created_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'), - 'meta' => json_encode([ - 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], - 'REMOTE_PORT' => $_SERVER['REMOTE_PORT'], - 'SERVER_NAME' => $_SERVER['SERVER_NAME'], - 'REQUEST_URI' => $_SERVER['REQUEST_URI'], - 'HTTP_HOST' => $_SERVER['HTTP_HOST'], - 'HTTP_USER_AGENT' => $_SERVER['HTTP_USER_AGENT'] - ]), + $this->longUrlRepo->registerAccess($url, [ + 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], + 'REMOTE_PORT' => $_SERVER['REMOTE_PORT'], + 'SERVER_NAME' => $_SERVER['SERVER_NAME'], + 'REQUEST_URI' => $_SERVER['REQUEST_URI'], + 'HTTP_HOST' => $_SERVER['HTTP_HOST'], + 'HTTP_USER_AGENT' => $_SERVER['HTTP_USER_AGENT'] ]); $newResponse = $response ->withHeader('Content-type', 'application/json') ->withStatus(302) - ->withHeader('Location', $row['long_url']); + ->withHeader('Location', $url->longUrl->value()); return $newResponse; } diff --git a/src/Adapter/Repository/Database/DbLongUrlRepository.php b/src/Adapter/Repository/Database/DbLongUrlRepository.php index c9ab9e2..2fac731 100644 --- a/src/Adapter/Repository/Database/DbLongUrlRepository.php +++ b/src/Adapter/Repository/Database/DbLongUrlRepository.php @@ -8,18 +8,22 @@ use App\Domain\Repository\LongUrlRepository; use App\Shared\Adapter\Contracts\DatabaseOrm; use App\Shared\Adapter\Contracts\UuidGenerator; +use DateTimeImmutable; final class DbLongUrlRepository implements LongUrlRepository { public function __construct( private DatabaseOrm $orm, - private UuidGenerator $uuidGenerator + private UuidGenerator $uuidGenerator, + private array $config ) {} public function shortLongUrl(LongUrl $url): LongUrl { while (true) { - $urlRecord = $this->orm->read('urls', ['short_url_path' => $url->getShortUrlPath()]); + $urlRecord = $this->orm->read('urls', ['short_url_path' => $url->getShortUrlPath()], [ + 'columns' => ['id'] + ]); if (!is_null($urlRecord)) { $url->renewShortUrlPath(); @@ -43,4 +47,35 @@ public function shortLongUrl(LongUrl $url): LongUrl return $url; } + + public function getUrlByPath(string $path):? LongUrl + { + $urlRecord = $this->orm->read('urls', ['short_url_path' => $path]); + + if (is_null($urlRecord)) { + return null; + } + + return LongUrl::create([ + 'id' => $urlRecord['uuid'], + 'longUrl' => $urlRecord['long_url'], + 'shortUrl' => $this->config['baseUrl'] . '/' . $urlRecord['short_url_path'], + 'baseUrlToShortUrl' => $this->config['baseUrl'], + 'type' => $urlRecord['type'], + 'createdAt' => $urlRecord['created_at'] + ]); + } + + public function registerAccess(LongUrl $url, array $metaInfo = []) + { + $uuid = $this->uuidGenerator->create(); + + $this->orm->create('urls_logs', [ + 'id' => $this->uuidGenerator->toBytes($uuid), + 'uuid' => $uuid, + 'url_id' => $this->uuidGenerator->toBytes($url->id), + 'created_at' => (new DateTimeImmutable())->format('Y-m-d H:i:s'), + 'meta' => json_encode($metaInfo) + ]); + } } \ No newline at end of file diff --git a/src/Domain/Entity/LongUrl.php b/src/Domain/Entity/LongUrl.php index 0c39295..9568605 100644 --- a/src/Domain/Entity/LongUrl.php +++ b/src/Domain/Entity/LongUrl.php @@ -21,8 +21,8 @@ final class LongUrl extends EntityBase { protected Url $baseUrlToShortUrl; protected Url $longUrl; - protected Url $shortUrl; protected LongUrlType $type; + protected ?Url $shortUrl; protected ?DateTimeInterface $createdAt; public static function create(array $values): EntityBase @@ -31,7 +31,10 @@ public static function create(array $values): EntityBase $values['createdAt'] = (new DateTimeImmutable())->format(DateTimeInterface::ATOM); } - $values['shortUrl'] = $values['baseUrlToShortUrl'] . '/' . self::generatePathToShortUrl(); + if (!isset($values['shortUrl'])) { + $values['shortUrl'] = $values['baseUrlToShortUrl'] . '/' . self::generatePathToShortUrl(); + } + return parent::create($values); } diff --git a/src/Domain/Repository/LongUrlRepository.php b/src/Domain/Repository/LongUrlRepository.php index 891d4f0..f075d53 100644 --- a/src/Domain/Repository/LongUrlRepository.php +++ b/src/Domain/Repository/LongUrlRepository.php @@ -9,4 +9,6 @@ interface LongUrlRepository { public function shortLongUrl(LongUrl $url): LongUrl; + public function getUrlByPath(string $path):? LongUrl; + public function registerAccess(LongUrl $url, array $metaInfo = []); } \ No newline at end of file diff --git a/src/Shared/Domain/EntityBase.php b/src/Shared/Domain/EntityBase.php index e85b052..0b000c9 100644 --- a/src/Shared/Domain/EntityBase.php +++ b/src/Shared/Domain/EntityBase.php @@ -19,10 +19,7 @@ */ abstract class EntityBase implements Entity { - /** - * @var string|int - */ - protected $id; + protected string | int | null $id = null; /** * Entity constructor. diff --git a/src/Shared/Infra/PdoOrm.php b/src/Shared/Infra/PdoOrm.php index a335ecd..4df2504 100644 --- a/src/Shared/Infra/PdoOrm.php +++ b/src/Shared/Infra/PdoOrm.php @@ -32,7 +32,39 @@ public function create(string $tableName, array $values): int|string public function read(string $tableName, array $filters, array $options = []): ?array { - return null; + $columns = '*'; + $clauses = ''; + + if (isset($options['columns'])) { + $columns = array_map(fn ($columnName) => '`' . $columnName . '`', $options['columns']); + $columns = implode(',', $columns); + } + + $filterColumns = array_keys($filters); + $i = 0; + + foreach ($filterColumns as $columnName) { + if ($i === 0) { + $clauses .= "WHERE `{$columnName}` = :{$columnName}"; + } + + if ($i > 0) { + $clauses .= "AND `{$columnName}` = :{$columnName}"; + } + + $i++; + } + + $sql = sprintf("SELECT %s FROM `%s` %s", $columns, $tableName, $clauses); + $stmt = $this->pdo->prepare($sql); + $stmt->execute($filters); + $row = $stmt->fetch(); + + if (!$row) { + return null; + } + + return $row; } public function update(string $tableName, array $values, array $conditions): bool From 2960c8f602e035127e779721d30618655dd44930 Mon Sep 17 00:00:00 2001 From: Kilderson Sena Date: Mon, 27 Sep 2021 09:18:02 -0300 Subject: [PATCH 5/5] last refactoring changes --- public_html/js/script.js | 31 +++++++++++++------ src/Adapter/Controllers/HomeController.php | 21 +++---------- .../Database/DbLongUrlRepository.php | 12 +++++++ src/Domain/Repository/LongUrlRepository.php | 1 + src/Shared/Adapter/Contracts/DatabaseOrm.php | 1 + src/Shared/Infra/PdoOrm.php | 8 +++++ 6 files changed, 49 insertions(+), 25 deletions(-) diff --git a/public_html/js/script.js b/public_html/js/script.js index 489e7dd..de4fdb6 100644 --- a/public_html/js/script.js +++ b/public_html/js/script.js @@ -3,6 +3,19 @@ $(document).ready(function () { if (urlHistory) { urlHistory = JSON.parse(urlHistory); + + urlHistory = urlHistory.map(row => { + if (!row.hasOwnProperty('huge')) return row; + return { + longUrl: row.huge, + shortenedUrl: row.shortened, + createdAt: row.created_at, + economyRate: row.economyRate, + }; + }); + + window.localStorage.setItem('url_history', JSON.stringify(urlHistory)); + for (let index in urlHistory) { if (index === '5') break; $('div.shortened-urls ul').append(getHistoryItemTemplate(urlHistory[index])); @@ -58,7 +71,7 @@ $(document).ready(function () { $.ajax({ url: `${baseUrl}/api/public/shorten`, type: 'post', - data: { huge_url : $inputUrl.val() }, + data: { long_url : $inputUrl.val() }, beforeSend : function() { $inputUrl.prop('disabled', true); $btnShorten.prop('disabled', true); @@ -85,15 +98,15 @@ $(document).ready(function () { const $divResult = $('div.shortened-url-result'); $divResult.css('display', 'flex'); - $divResult.find('a').attr('href', payload.data.shortened); - $divResult.find('a span.url-text').html(payload.data.shortened); + $divResult.find('a').attr('href', payload.data.shortenedUrl); + $divResult.find('a span.url-text').html(payload.data.shortenedUrl); $divResult.find('a span.badge').html(`-${payload.data.economyRate}%`); - $divResult.find('button').attr('data-url', payload.data.shortened); + $divResult.find('button').attr('data-url', payload.data.shortenedUrl); }).fail(function(jqXHR, textStatus, msg) { if (jqXHR.status === 400) { const payload = jqXHR.responseJSON; let message = ''; - switch (payload.data.huge_url) { + switch (payload.data.longUrl) { case 'invalid-url': message = 'Insira ua URL vĂ¡lida com "http://" ou "https://" para poder encurtar.'; break; @@ -132,16 +145,16 @@ $(document).ready(function () { function getHistoryItemTemplate(data) { return `
  • -
    ${data.huge.substring(0, 38)}...
    +
    ${data.longUrl.substring(0, 38)}...
    -
    diff --git a/src/Adapter/Controllers/HomeController.php b/src/Adapter/Controllers/HomeController.php index 2768cc2..e22a894 100644 --- a/src/Adapter/Controllers/HomeController.php +++ b/src/Adapter/Controllers/HomeController.php @@ -5,6 +5,7 @@ namespace App\Adapter\Controllers; use PDO; +use App\Domain\Repository\LongUrlRepository; use Psr\Container\ContainerInterface; use Psr\Http\Message\RequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; @@ -12,30 +13,18 @@ final class HomeController { - private PDO $db; private Twig $view; + private LongUrlRepository $longUrlRepo; public function __construct(ContainerInterface $container) { - $this->db = $container->get('db'); $this->view = $container->get('view'); + $this->longUrlRepo = $container->get(LongUrlRepository::class); } public function __invoke(Request $request, Response $response, array $args): Response { - $stmt = $this->db->prepare(trim(" - select count(*) as total_urls from urls - union all - select count(*) as total_clicks from urls_logs - ")); - - $stmt->execute(); - - [$totalUrls, $totalClicks] = $stmt->fetchAll(PDO::FETCH_COLUMN); - - return $this->view->render($response, 'index.html.twig', [ - 'totalUrls' => ceil($totalUrls), - 'totalClicks' => ceil($totalClicks) - ]); + $rows = $this->longUrlRepo->countUrlsAndClicks(); + return $this->view->render($response, 'index.html.twig', $rows); } } \ No newline at end of file diff --git a/src/Adapter/Repository/Database/DbLongUrlRepository.php b/src/Adapter/Repository/Database/DbLongUrlRepository.php index 2fac731..dceb1eb 100644 --- a/src/Adapter/Repository/Database/DbLongUrlRepository.php +++ b/src/Adapter/Repository/Database/DbLongUrlRepository.php @@ -9,6 +9,7 @@ use App\Shared\Adapter\Contracts\DatabaseOrm; use App\Shared\Adapter\Contracts\UuidGenerator; use DateTimeImmutable; +use PDO; final class DbLongUrlRepository implements LongUrlRepository { @@ -78,4 +79,15 @@ public function registerAccess(LongUrl $url, array $metaInfo = []) 'meta' => json_encode($metaInfo) ]); } + + public function countUrlsAndClicks(): array + { + $sql = "select count(*) as `total_urls` from `urls` union all select count(*) as `total_clicks` from `urls_logs`"; + $rows = $this->orm->querySql($sql, [], ['fetchMode' => PDO::FETCH_COLUMN]); + + return [ + 'totalUrls' => ceil($rows[0]), + 'totalClicks' => ceil($rows[1]) + ]; + } } \ No newline at end of file diff --git a/src/Domain/Repository/LongUrlRepository.php b/src/Domain/Repository/LongUrlRepository.php index f075d53..396f0ad 100644 --- a/src/Domain/Repository/LongUrlRepository.php +++ b/src/Domain/Repository/LongUrlRepository.php @@ -11,4 +11,5 @@ interface LongUrlRepository public function shortLongUrl(LongUrl $url): LongUrl; public function getUrlByPath(string $path):? LongUrl; public function registerAccess(LongUrl $url, array $metaInfo = []); + public function countUrlsAndClicks(): array; } \ No newline at end of file diff --git a/src/Shared/Adapter/Contracts/DatabaseOrm.php b/src/Shared/Adapter/Contracts/DatabaseOrm.php index ec53df5..547db6d 100644 --- a/src/Shared/Adapter/Contracts/DatabaseOrm.php +++ b/src/Shared/Adapter/Contracts/DatabaseOrm.php @@ -12,4 +12,5 @@ public function update(string $tableName, array $values, array $conditions): boo public function delete(string $tableName, array $conditions): bool; public function search(string $tableName, array $filters, array $options = []): array; public function persist(string $tableName, array $values): int | string; + public function querySql(string $sql, array $values = [], array $options = []): array; } \ No newline at end of file diff --git a/src/Shared/Infra/PdoOrm.php b/src/Shared/Infra/PdoOrm.php index 4df2504..3df7fe6 100644 --- a/src/Shared/Infra/PdoOrm.php +++ b/src/Shared/Infra/PdoOrm.php @@ -86,4 +86,12 @@ public function persist(string $tableName, array $values): int|string { return ''; } + + public function querySql(string $sql, array $values = [], array $options = []): array + { + $stmt = $this->pdo->prepare(trim($sql)); + $stmt->execute($values); + $fetchMode = $options['fetchMode'] ?? PDO::FETCH_ASSOC; + return $stmt->fetchAll($fetchMode); + } } \ No newline at end of file