diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index ac8402a..84735b7 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -37,7 +37,7 @@ jobs: run: composer config --no-plugins allow-plugins.pestphp/pest-plugin true - name: Install dependencies - run: composer install --prefer-dist --no-interaction --no-progress + run: composer install --prefer-dist --no-interaction --no-progress --ignore-platform-reqs - name: Copy environment file run: cp .env.example .env @@ -46,4 +46,4 @@ jobs: run: composer test - name: PHPStan - run: composer phpstan \ No newline at end of file + run: composer phpstan diff --git a/composer.json b/composer.json index 2cec473..3902234 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ "monolog/monolog": "^2 || ^3", "vlucas/phpdotenv": "^v5.1", "league/container": "^4", - "zircote/swagger-php": "^5.0" + "zircote/swagger-php": "^5.0", + "laminas/laminas-db": "^2.20" }, "require-dev": { "pestphp/pest": "^3.0", diff --git a/config/containers/database.container.php b/config/containers/database.container.php index e5ab000..0d47b31 100644 --- a/config/containers/database.container.php +++ b/config/containers/database.container.php @@ -1,12 +1,13 @@ add(PDO::class) - ->addArgument('sqlite:'.storage_path('database.sqlite')) - ->addMethodCall('setAttribute', [PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION]) - ->addMethodCall('setAttribute', [PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC]) - ->addMethodCall('setAttribute', [PDO::ATTR_EMULATE_PREPARES, false]); + ->add(AdapterInterface::class, Adapter::class) + ->addArgument([ + 'driver' => 'Pdo_Sqlite', + 'dsn' => 'sqlite:'.storage_path('database.sqlite') + ]); }; diff --git a/src/Model/Album.php b/src/Model/Album.php index 2a1aef4..daa5fe8 100644 --- a/src/Model/Album.php +++ b/src/Model/Album.php @@ -5,12 +5,9 @@ use OpenApi\Attributes as OA; #[OA\Schema] -class Album +class Album extends Model { - #[OA\Property(example: 42, nullable: false)] - public int $id; - #[OA\Property(example: 'Minha História', nullable: false)] public string $title; diff --git a/src/Model/Artist.php b/src/Model/Artist.php index b6c2b6a..7840117 100644 --- a/src/Model/Artist.php +++ b/src/Model/Artist.php @@ -5,12 +5,9 @@ use OpenApi\Attributes as OA; #[OA\Schema] -class Artist +class Artist extends Model { - #[OA\Property(example: 1, nullable: false)] - public int $id; - #[OA\Property(example: 'AC/DC', nullable: false)] public string $name; } \ No newline at end of file diff --git a/src/Model/Model.php b/src/Model/Model.php new file mode 100644 index 0000000..e43f596 --- /dev/null +++ b/src/Model/Model.php @@ -0,0 +1,13 @@ +table_gateway = new TableGateway($this->getTable(), $adapter); + $this->logger = $logger->withName(__CLASS__); + } + + abstract protected function getTable(): string; + + /** + * @return array> + */ + public function all(): array + { + return iterator_to_array($this->table_gateway->select()); + } + + /** + * @return array|null + */ + public function find(int $id): ?array + { + /** @var ResultSet $results */ + $results = $this->table_gateway->select([static::ROW_IDENTIFIER => $id]); + + return $results->current(); + } + + public function create(array $data): int + { + $this->table_gateway->insert($data); + + return $this->table_gateway->getLastInsertValue(); + } + + public function update(int $id, array $data): bool + { + return $this->table_gateway->update($data, [static::ROW_IDENTIFIER => $id]) > 0; + } + + public function delete(int $id): bool + { + return $this->table_gateway->delete([static::ROW_IDENTIFIER => $id]) > 0; + } +} diff --git a/src/Repository/AlbumRepository.php b/src/Repository/AlbumRepository.php index 97d87db..d4aac8d 100644 --- a/src/Repository/AlbumRepository.php +++ b/src/Repository/AlbumRepository.php @@ -2,92 +2,13 @@ namespace App\Repository; -use App\Model\Artist; -use InvalidArgumentException; -use Monolog\Logger; -use PDO; -use RuntimeException; - -readonly class AlbumRepository implements AlbumRepositoryInterface +readonly class AlbumRepository extends AbstractRepository { - private Logger $logger; - - public function __construct( - private PDO $pdo, - Logger $logger - ) { - $this->logger = $logger->withName(__CLASS__); - } - - /** @return Artist[] */ - public function all(): array - { - return $this - ->pdo - ->query('SELECT AlbumId AS id, Title AS title, ArtistId as artist_id FROM albums') - ->fetchAll(PDO::FETCH_CLASS, Artist::class); - } - - public function find(int $id): ?Artist - { - $stmt = $this - ->pdo - ->prepare('SELECT AlbumId AS id, Title AS title, ArtistId as artist_id FROM albums WHERE AlbumId = ?'); - - $stmt->execute([$id]); - - return $stmt->fetchObject(Artist::class) ?: null; - } - - public function create(array $data): int - { - $stmt = $this - ->pdo - ->prepare('INSERT INTO albums (Title, ArtistId) VALUES (?, ?)'); - - if (!$stmt->execute([$data['title'], $data['artist_id']])) { - $this->logger->error('An error occurred while trying to create an album with data: {data}', ['{data}' => implode(', ', array_keys($data))]); - throw new RuntimeException('Error creating album', 500); - } - - return (int)$this->pdo->lastInsertId(); - } + public const ROW_IDENTIFIER = 'AlbumId'; - public function update(int $id, array $data): bool + protected function getTable(): string { - $fields = []; - $values = []; - - if (isset($data['title'])) { - $fields[] = 'Title = ?'; - $values[] = $data['title']; - } - - if (isset($data['artist_id'])) { - $fields[] = 'ArtistId = ?'; - $values[] = $data['artist_id']; - } - - if (empty($fields)) { - $this->logger->error('No data provided to update Album #{id}', ['{id}' => $id]); - throw new InvalidArgumentException('No data to update'); - } - - $values[] = $id; - $sql = 'UPDATE albums SET '.implode(', ', $fields).' WHERE AlbumId = ?'; - - $stmt = $this->pdo->prepare($sql); - - return $stmt->execute($values); - } - - public function delete(int $id): bool - { - $stmt = $this - ->pdo - ->prepare('DELETE FROM albums WHERE AlbumId = ?'); - - return $stmt->execute([$id]); + return 'albums'; } } diff --git a/src/Repository/AlbumRepositoryInterface.php b/src/Repository/AlbumRepositoryInterface.php deleted file mode 100644 index 2e4f4c7..0000000 --- a/src/Repository/AlbumRepositoryInterface.php +++ /dev/null @@ -1,22 +0,0 @@ -logger = $logger->withName(__CLASS__); - } - - /** @return Artist[] */ - public function all(): array - { - return $this - ->pdo - ->query('SELECT ArtistId AS id, Name AS name FROM artists') - ->fetchAll(PDO::FETCH_CLASS, Artist::class); - } - - public function find(int $id): ?Artist - { - $stmt = $this - ->pdo - ->prepare('SELECT ArtistId AS id, Name AS name FROM artists WHERE ArtistId = ?'); - - $stmt->execute([$id]); - - return $stmt->fetchObject(Artist::class) ?: null; - } - - public function create(array $data): int - { - $stmt = $this - ->pdo - ->prepare('INSERT INTO artists (Name) VALUES (?)'); - - if (!$stmt->execute([$data['name']])) { - $this->logger->error('An error occurred while trying to create an artist with data: {data}', ['{data}' => implode(', ', array_keys($data))]); - throw new RuntimeException('Error creating artist', 500); - } + public const ROW_IDENTIFIER = 'ArtistId'; - return (int)$this->pdo->lastInsertId(); - } - - public function update(int $id, array $data): bool + protected function getTable(): string { - if (!isset($data['name'])) { - $this->logger->error('No data provided to update Artist #{id}', ['{id}' => $id]); - throw new RuntimeException('No fields to update', 400); - } - - $stmt = $this - ->pdo - ->prepare('UPDATE artists SET Name = ? WHERE ArtistId = ?'); - - return $stmt->execute([$data['name'], $id]); - } - - public function delete(int $id): bool - { - $stmt = $this - ->pdo - ->prepare('DELETE FROM artists WHERE ArtistId = ?'); - - return $stmt->execute([$id]); + return 'artists'; } } diff --git a/src/Repository/ArtistRepositoryInterface.php b/src/Repository/ArtistRepositoryInterface.php deleted file mode 100644 index 1dea9c1..0000000 --- a/src/Repository/ArtistRepositoryInterface.php +++ /dev/null @@ -1,22 +0,0 @@ - $object */ + public static function toAlbum(iterable $object): Album + { + $album = new Album(); + $album->id = $object[AlbumRepository::ROW_IDENTIFIER] ?? null; + $album->title = $object['Title'] ?? null; + $album->artist_id = $object['ArtistId'] ?? null; + + return $album; + } +} diff --git a/src/Repository/Mapper/ArtistMapper.php b/src/Repository/Mapper/ArtistMapper.php new file mode 100644 index 0000000..13627f8 --- /dev/null +++ b/src/Repository/Mapper/ArtistMapper.php @@ -0,0 +1,21 @@ + $object */ + public static function toArtist(iterable $object): Artist + { + $artist = new Artist(); + $artist->id = $object[ArtistRepository::ROW_IDENTIFIER] ?? null; + $artist->name = $object['Name'] ?? null; + + return $artist; + } + +} diff --git a/src/Repository/RepositoryInterface.php b/src/Repository/RepositoryInterface.php new file mode 100644 index 0000000..38a59c1 --- /dev/null +++ b/src/Repository/RepositoryInterface.php @@ -0,0 +1,23 @@ +> */ + public function all(): array; + + /** @return array|null */ + public function find(int $id): ?array; + + /** @param array{Title?: string, ArtistId?: int, Name?: string} $data */ + public function create(array $data): int; + + /** @param array{Title?: string, ArtistId?: int} $data */ + public function update(int $id, array $data): bool; + + public function delete(int $id): bool; +} diff --git a/src/Service/AlbumService.php b/src/Service/AlbumService.php index f1bbb39..05446f5 100644 --- a/src/Service/AlbumService.php +++ b/src/Service/AlbumService.php @@ -2,8 +2,9 @@ namespace App\Service; -use App\Model\Artist; +use App\Model\Album; use App\Repository\AlbumRepository; +use App\Repository\Mapper\AlbumMapper; use InvalidArgumentException; use Monolog\Logger; use RuntimeException; @@ -20,13 +21,16 @@ public function __construct( $this->logger = $logger->withName(__CLASS__); } - /** @return Artist[] */ + /** @return Album[] */ public function all(): array { - return $this->repository->all(); + return array_map( + fn(iterable $album): Album => AlbumMapper::toAlbum($album), + $this->repository->all() + ); } - public function find(int $id): ?Artist + public function find(int $id): ?Album { $album = $this->repository->find($id); if ($album === null) { @@ -34,52 +38,57 @@ public function find(int $id): ?Artist throw new RuntimeException('Album does not exist', 404); } - return $this->repository->find($id); + return AlbumMapper::toAlbum($album); } /** @param array{title: string, artist_id: int} $data */ - public function create(array $data): Artist + public function create(array $data): Album { if (!isset($data['title'], $data['artist_id'])) { $this->logger->error('Missing required data, unable to save Album, provided data: {data}', ['{data}' => implode(', ', array_keys($data))]); throw new InvalidArgumentException('Missing data, "title" and "artist_id" fields are required'); } - $id = $this->repository->create($data); + $id = $this->repository->create(['Title' => $data['title'], 'ArtistId' => $data['artist_id']]); if ($id === 0) { $this->logger->error('Unable to create album, provided data: {data}', ['{data}' => implode(', ', array_keys($data))]); throw new RuntimeException('Album could not be created', 500); } - return $this->repository->find($id); + return $this->find($id); } /** @param array{title?: string, artist_id?: int} $data */ - public function update(int $id, array $data): Artist + public function update(int $id, array $data): Album { if (!isset($data['title']) && !isset($data['artist_id'])) { $this->logger->error('Missing required data, unable to update Album, provided data: {data}', ['{data}' => implode(', ', array_keys($data))]); throw new InvalidArgumentException('Missing data, "title" and/or "artist_id" fields are required'); } - $album = $this->repository->find($id); - if ($album === null) { + $album = $this->find($id); + if (!$album instanceof Album) { $this->logger->error('Album with ID #{id} not found', ['{id}' => $id]); throw new RuntimeException('Album does not exist', 404); } + $data = array_filter([ + 'Title' => $data['title'] ?? null, + 'ArtistId' => $data['artist_id'] ?? null, + ]); + if (!$this->repository->update($id, $data)) { $this->logger->error('Album could not be updated with provided data: {data}', ['{data}' => implode(', ', array_keys($data))]); throw new RuntimeException('Album could not be updated', 500); } - return $this->repository->find($id); + return $this->find($id); } public function delete(int $id): bool { - $album = $this->repository->find($id); - if ($album === null) { + $album = $this->find($id); + if (!$album instanceof Album) { $this->logger->error('Album with ID #{id} not found', ['{id}' => $id]); throw new RuntimeException('Album does not exist', 404); } diff --git a/src/Service/ArtistService.php b/src/Service/ArtistService.php index 3740751..252b99b 100644 --- a/src/Service/ArtistService.php +++ b/src/Service/ArtistService.php @@ -4,6 +4,7 @@ use App\Model\Artist; use App\Repository\ArtistRepository; +use App\Repository\Mapper\ArtistMapper; use InvalidArgumentException; use Monolog\Logger; use RuntimeException; @@ -23,7 +24,10 @@ public function __construct( /** @return Artist[] */ public function all(): array { - return $this->repository->all(); + return array_map( + fn(iterable $artist): Artist => ArtistMapper::toArtist($artist), + $this->repository->all() + ); } public function find(int $id): ?Artist @@ -34,7 +38,7 @@ public function find(int $id): ?Artist throw new RuntimeException('Artist does not exist', 404); } - return $artist; + return ArtistMapper::toArtist($artist); } /** @param array{name: string} $data */ @@ -45,13 +49,13 @@ public function create(array $data): Artist throw new InvalidArgumentException('Missing data, "name" field is required'); } - $id = $this->repository->create($data); + $id = $this->repository->create(['Name' => $data['name']]); if ($id === 0) { $this->logger->error('Unable to create artist, provided data: {data}', ['data' => implode(', ', array_keys($data))]); throw new RuntimeException('Artist could not be created', 500); } - return $this->repository->find($id); + return $this->find($id); } /** @param array{name: string} $data */ @@ -62,8 +66,8 @@ public function update(int $id, array $data): Artist throw new InvalidArgumentException('Missing data, "name" field is required'); } - $artist = $this->repository->find($id); - if ($artist === null) { + $artist = $this->find($id); + if (!$artist instanceof Artist) { $this->logger->error('Artist with ID #{id} not found', ['{id}' => $id]); throw new RuntimeException('Artist does not exist', 404); } @@ -73,13 +77,13 @@ public function update(int $id, array $data): Artist throw new RuntimeException('Artist could not be updated', 500); } - return $this->repository->find($id); + return $this->find($id); } public function delete(int $id): bool { - $artist = $this->repository->find($id); - if ($artist === null) { + $artist = $this->find($id); + if (!$artist instanceof Artist) { $this->logger->error('Artist with ID #{id} not found', ['{id}' => $id]); throw new RuntimeException('Artist does not exist', 404); }