├── src ├── Database │ ├── Seeds │ │ ├── .gitkeep │ │ ├── AlbumSeeder.php │ │ └── TrackSeeder.php │ └── Migrations │ │ ├── .gitkeep │ │ ├── 2020-03-06-003734_Album.php │ │ └── 2020-03-26-203957_Track.php ├── Domain │ ├── DMLRepository.php │ ├── AlbumTrackSummary │ │ ├── AlbumTrackSummaryRepository.php │ │ └── AlbumTrackSummary.php │ ├── Exception │ │ ├── RecordNotFoundException.php │ │ └── DuplicatedRecordException.php │ ├── Album │ │ ├── AlbumRepository.php │ │ ├── AlbumNotFoundException.php │ │ └── Album.php │ └── Track │ │ ├── TrackRepository.php │ │ ├── TrackDuplicatedRectorException.php │ │ ├── TrackNotFoundException.php │ │ └── Track.php ├── Views │ ├── layout.php │ ├── album-track-summary │ │ └── totalsong.php │ ├── album │ │ ├── add.php │ │ ├── edit.php │ │ └── index.php │ └── track │ │ ├── add.php │ │ ├── edit.php │ │ └── index.php ├── Config │ ├── Album.php │ ├── Services.php │ └── Routes.php ├── Infrastructure │ └── Persistence │ │ ├── DMLPersistence.php │ │ ├── AlbumTrackSummary │ │ └── SQLAlbumTrackSummaryRepository.php │ │ ├── Album │ │ └── SQLAlbumRepository.php │ │ └── Track │ │ └── SQLTrackRepository.php ├── Controllers │ ├── AlbumTrackSummary.php │ ├── Album.php │ └── Track.php └── Models │ ├── TrackModel.php │ └── AlbumModel.php ├── .github ├── FUNDING.yml └── workflows │ └── ci_build.yaml ├── .gitignore ├── .php-cs-fixer.dist.php ├── phpstan.neon ├── infection.json.dist ├── CONTRIBUTING.md ├── test ├── unit │ └── Domain │ │ ├── Album │ │ ├── AlbumTest.php │ │ └── AlbumNotFoundExceptionTest.php │ │ ├── Track │ │ ├── TrackNotFoundExceptionTest.php │ │ └── TrackTest.php │ │ └── AlbumTrackSummary │ │ └── AlbumTrackSummaryTest.php ├── Database │ └── Infrastructure │ │ └── Persistence │ │ ├── AlbumTrackSummary │ │ └── SQLAlbumTrackSummaryRepositoryTest.php │ │ ├── Album │ │ └── SQLAlbumRepositoryTest.php │ │ └── Track │ │ └── SQLTrackRepositoryTest.php └── Controller │ ├── AlbumTrackSummaryTest.php │ ├── AlbumTest.php │ └── TrackTest.php ├── LICENSE ├── rector.php ├── phpcs.xml ├── composer.json ├── phpunit.xml.dist ├── phpunit.xml.github-actions.dist └── README.md /src/Database/Seeds/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Database/Migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: samsonasik 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | .phpunit.result.cache 4 | build/logs 5 | phpunit.xml 6 | .php_cs_cache 7 | infection.log 8 | .php-cs-fixer.cache 9 | /.phpunit.cache -------------------------------------------------------------------------------- /src/Domain/DMLRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain; 13 | 14 | interface DMLRepository 15 | { 16 | public function save(array $data): bool; 17 | 18 | public function deleteOfId(int $id): bool; 19 | } 20 | -------------------------------------------------------------------------------- /src/Views/layout.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CI4 App - <?php echo $this->getData()['title'] ?? ''; ?> 5 | 6 | 7 | 8 | 9 | 10 | renderSection('content') ?> 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Domain/AlbumTrackSummary/AlbumTrackSummaryRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\AlbumTrackSummary; 13 | 14 | interface AlbumTrackSummaryRepository 15 | { 16 | public function findPaginatedSummaryTotalSongData(): ?array; 17 | } 18 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | files() 9 | ->in([ 10 | __DIR__ . '/src', 11 | __DIR__ . '/test', 12 | ]) 13 | ->exclude(['Views']); 14 | 15 | $options = [ 16 | 'finder' => $finder, 17 | ]; 18 | 19 | return Factory::create(new CodeIgniter4(), [], $options) 20 | ->forLibrary('samsonasik/ci4-album', 'Abdul Malik Ikhsan', 'samsonasik@gmail.com', 2020); -------------------------------------------------------------------------------- /src/Config/Album.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Config; 13 | 14 | use CodeIgniter\Config\BaseConfig; 15 | 16 | final class Album extends BaseConfig 17 | { 18 | /** 19 | * @var int 20 | */ 21 | public $paginationPerPage = 10; 22 | } 23 | -------------------------------------------------------------------------------- /src/Domain/Exception/RecordNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\Exception; 13 | 14 | use DomainException; 15 | 16 | class RecordNotFoundException extends DomainException 17 | { 18 | protected function __construct(string $message) 19 | { 20 | $this->message = $message; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Domain/Album/AlbumRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\Album; 13 | 14 | use Album\Domain\DMLRepository; 15 | 16 | interface AlbumRepository extends DMLRepository 17 | { 18 | public function findPaginatedData(string $keyword = ''): ?array; 19 | 20 | public function findAlbumOfId(int $id): Album; 21 | } 22 | -------------------------------------------------------------------------------- /src/Domain/Exception/DuplicatedRecordException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\Exception; 13 | 14 | use DomainException; 15 | 16 | class DuplicatedRecordException extends DomainException 17 | { 18 | protected function __construct(string $message) 19 | { 20 | $this->message = $message; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Domain/Track/TrackRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\Track; 13 | 14 | use Album\Domain\Album\Album; 15 | use Album\Domain\DMLRepository; 16 | 17 | interface TrackRepository extends DMLRepository 18 | { 19 | public function findPaginatedData(Album $album, string $keyword = ''): ?array; 20 | 21 | public function findTrackOfId(int $id): Track; 22 | } 23 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | bootstrapFiles: 3 | - vendor/codeigniter4/framework/system/Test/bootstrap.php 4 | inferPrivatePropertyTypeFromConstructor: true 5 | excludePaths: 6 | - src/Config/* 7 | - src/Database/* 8 | - src/Views/* 9 | ignoreErrors: 10 | - '#Access to protected property [a-zA-Z0-9\\_]+Model::\$returnType.#' 11 | - '#Call to an undefined method CodeIgniter\\Model::get\(\)#' 12 | 13 | - 14 | identifier: missingType.iterableValue 15 | 16 | - 17 | identifier: method.nonObject 18 | path: src/Infrastructure/Persistence/Track/SQLTrackRepository.php -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ], 6 | "excludes": [ 7 | "Config", 8 | "Database", 9 | "Views" 10 | ] 11 | }, 12 | "logs": { 13 | "text": "infection.log", 14 | "stryker": { 15 | "badge": "master" 16 | } 17 | }, 18 | "mutators": { 19 | "@default": true, 20 | "PublicVisibility": false, 21 | "MethodCallRemoval": false, 22 | "ArrayItem": false, 23 | "ArrayItemRemoval": false 24 | }, 25 | "bootstrap":"./vendor/codeigniter4/framework/system/Test/bootstrap.php" 26 | } -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/DMLPersistence.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Infrastructure\Persistence; 13 | 14 | use CodeIgniter\Model; 15 | 16 | /** 17 | * @internal 18 | * 19 | * @property Model $model 20 | */ 21 | trait DMLPersistence 22 | { 23 | public function save(?array $data = null): bool 24 | { 25 | return $this->model->save(new $this->model->returnType($data)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | CONTRIBUTING 2 | ------------ 3 | To contribute, you can send pull requests with : 4 | 5 | - Typo fix. 6 | - Use [CodeIgniter 4 Coding Standard](https://github.com/codeigniter4/coding-standard), you can fix and check with run commands: 7 | 8 | ```bash 9 | # fix your code style 10 | composer cs-fix 11 | 12 | # check if any error that can't be fixed with cs-fix 13 | # that you need to manually fix 14 | composer cs-check 15 | ``` 16 | - Ensure the phpstan check shows No errors with run command: 17 | 18 | ```bash 19 | composer analyze 20 | ``` 21 | 22 | - patch(es) need new/updated test(s) that ensure there is no regression. 23 | - new feature(s) need test(s) that ensure there is no regression. -------------------------------------------------------------------------------- /src/Domain/Album/AlbumNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\Album; 13 | 14 | use Album\Domain\Exception\RecordNotFoundException; 15 | 16 | final class AlbumNotFoundException extends RecordNotFoundException 17 | { 18 | public static function forAlbumDoesnotExistOfId(int $id): self 19 | { 20 | return new self(sprintf( 21 | 'The album with album ID %d you requested does not exist.', 22 | $id, 23 | )); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Domain/Track/TrackDuplicatedRectorException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\Track; 13 | 14 | use Album\Domain\Exception\DuplicatedRecordException; 15 | 16 | final class TrackDuplicatedRectorException extends DuplicatedRecordException 17 | { 18 | public static function forDuplicatedTitle(int $id): self 19 | { 20 | return new self(sprintf( 21 | 'The track with album id %d has duplicated title.', 22 | $id, 23 | )); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Domain/Track/TrackNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\Track; 13 | 14 | use Album\Domain\Exception\RecordNotFoundException; 15 | 16 | final class TrackNotFoundException extends RecordNotFoundException 17 | { 18 | public static function forAlbumTrackDoesnotExistOfId(int $id): self 19 | { 20 | return new self(sprintf( 21 | 'The album track with track ID %d you requested does not exist.', 22 | $id, 23 | )); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Database/Seeds/AlbumSeeder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Database\Seeds; 13 | 14 | use CodeIgniter\Database\Seeder; 15 | 16 | final class AlbumSeeder extends Seeder 17 | { 18 | /** 19 | * @var array 20 | */ 21 | private const ROW = [ 22 | 'artist' => 'Sheila On 7', 23 | 'title' => 'Kisah Klasik Untuk Masa Depan', 24 | ]; 25 | 26 | public function run(): void 27 | { 28 | $this->db->table('album')->insert(self::ROW); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Database/Seeds/TrackSeeder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Database\Seeds; 13 | 14 | use CodeIgniter\Database\Seeder; 15 | 16 | final class TrackSeeder extends Seeder 17 | { 18 | /** 19 | * @var array 20 | */ 21 | private const ROW = [ 22 | 'album_id' => 1, 23 | 'title' => 'Sebuah Kisah Klasik', 24 | 'author' => 'Eross Chandra', 25 | ]; 26 | 27 | public function run(): void 28 | { 29 | $this->db->table('track')->insert(self::ROW); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Domain/AlbumTrackSummary/AlbumTrackSummary.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\AlbumTrackSummary; 13 | 14 | use CodeIgniter\Entity\Entity; 15 | 16 | /** 17 | * @property int $artist 18 | * @property int $id 19 | * @property string $title 20 | * @property string $total_song 21 | */ 22 | final class AlbumTrackSummary extends Entity 23 | { 24 | /** 25 | * @var array 26 | */ 27 | protected $attributes = [ 28 | 'id' => null, 29 | 'artist' => null, 30 | 'title' => null, 31 | 'total_song' => null, 32 | ]; 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/Domain/Album/AlbumTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Unit\Domain\Album; 13 | 14 | use Album\Domain\Album\Album; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | /** 18 | * @internal 19 | */ 20 | final class AlbumTest extends TestCase 21 | { 22 | public function testFillGetAttributes(): void 23 | { 24 | $album = new Album([ 25 | 'id' => 1, 26 | 'artist' => 'sheila on 7', 27 | 'title' => 'kisah klasik untuk masa depan', 28 | ]); 29 | 30 | $this->assertSame(1, $album->id); 31 | $this->assertSame('Sheila On 7', $album->artist); 32 | $this->assertSame('Kisah Klasik Untuk Masa Depan', $album->title); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Domain/Album/Album.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\Album; 13 | 14 | use CodeIgniter\Entity\Entity; 15 | 16 | /** 17 | * @property string $artist 18 | * @property int $id 19 | * @property string $title 20 | */ 21 | final class Album extends Entity 22 | { 23 | /** 24 | * @var array 25 | */ 26 | protected $attributes = [ 27 | 'id' => null, 28 | 'artist' => null, 29 | 'title' => null, 30 | ]; 31 | 32 | public function setArtist(string $artist): void 33 | { 34 | $this->attributes['artist'] = ucwords($artist); 35 | } 36 | 37 | public function setTitle(string $title): void 38 | { 39 | $this->attributes['title'] = ucwords($title); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/unit/Domain/Album/AlbumNotFoundExceptionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Unit\Domain\Album; 13 | 14 | use Album\Domain\Album\AlbumNotFoundException; 15 | use Error; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class AlbumNotFoundExceptionTest extends TestCase 22 | { 23 | public function testCannotInstantiateDirectly(): void 24 | { 25 | $this->expectException(Error::class); 26 | new AlbumNotFoundException('message'); 27 | } 28 | 29 | public function testInstantiateforAlbumDoesnotExistOfId(): void 30 | { 31 | $this->assertInstanceOf( 32 | AlbumNotFoundException::class, 33 | AlbumNotFoundException::forAlbumDoesnotExistOfId(1), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/unit/Domain/Track/TrackNotFoundExceptionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Unit\Domain\Track; 13 | 14 | use Album\Domain\Track\TrackNotFoundException; 15 | use Error; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | /** 19 | * @internal 20 | */ 21 | final class TrackNotFoundExceptionTest extends TestCase 22 | { 23 | public function testCannotInstantiateDirectly(): void 24 | { 25 | $this->expectException(Error::class); 26 | new TrackNotFoundException('message'); 27 | } 28 | 29 | public function testInstantiateforAlbumTrackDoesnotExistOfId(): void 30 | { 31 | $this->assertInstanceOf( 32 | TrackNotFoundException::class, 33 | TrackNotFoundException::forAlbumTrackDoesnotExistOfId(1), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/unit/Domain/Track/TrackTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Unit\Domain\Track; 13 | 14 | use Album\Domain\Track\Track; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | /** 18 | * @internal 19 | */ 20 | final class TrackTest extends TestCase 21 | { 22 | public function testFillGetAttributes(): void 23 | { 24 | $track = new Track([ 25 | 'id' => 1, 26 | 'album_id' => 1, 27 | 'title' => 'sebuah kisah klasik', 28 | 'author' => 'eross chandra', 29 | ]); 30 | 31 | $this->assertSame(1, $track->id); 32 | $this->assertSame(1, $track->album_id); 33 | $this->assertSame('Sebuah Kisah Klasik', $track->title); 34 | $this->assertSame('Eross Chandra', $track->author); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Domain/Track/Track.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Domain\Track; 13 | 14 | use CodeIgniter\Entity\Entity; 15 | 16 | /** 17 | * @property int $album_id 18 | * @property string $author 19 | * @property int $id 20 | * @property string $title 21 | */ 22 | final class Track extends Entity 23 | { 24 | /** 25 | * @var array 26 | */ 27 | protected $attributes = [ 28 | 'id' => null, 29 | 'album_id' => null, 30 | 'title' => null, 31 | 'author' => null, 32 | ]; 33 | 34 | public function setTitle(string $title): void 35 | { 36 | $this->attributes['title'] = ucwords($title); 37 | } 38 | 39 | public function setAuthor(string $author): void 40 | { 41 | $this->attributes['author'] = ucwords($author); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Abdul Malik Ikhsan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | use Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector; 13 | use Rector\Config\RectorConfig; 14 | 15 | return RectorConfig::configure() 16 | ->withPreparedSets( 17 | codeQuality: true, 18 | deadCode: true, 19 | typeDeclarations: true, 20 | privatization: true, 21 | naming: true, 22 | codingStyle: true 23 | ) 24 | ->withPhpSets(php82: true) 25 | ->withComposerBased(phpunit: true) 26 | ->withPaths([__DIR__ . '/src', __DIR__ . '/test']) 27 | ->withRootFiles() 28 | ->withImportNames(removeUnusedImports: true) 29 | ->withSkip([ 30 | // conflict with cs fix 31 | NewlineAfterStatementRector::class, 32 | ]) 33 | ->withBootstrapFiles( 34 | [__DIR__ . '/vendor/codeigniter4/framework/system/Test/bootstrap.php'] 35 | ) 36 | ->withPHPStanConfigs([__DIR__ . '/phpstan.neon']); 37 | -------------------------------------------------------------------------------- /src/Controllers/AlbumTrackSummary.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Controllers; 13 | 14 | use Album\Config\Services; 15 | use Album\Domain\AlbumTrackSummary\AlbumTrackSummaryRepository; 16 | use Album\Models\AlbumModel; 17 | use App\Controllers\BaseController; 18 | 19 | final class AlbumTrackSummary extends BaseController 20 | { 21 | private readonly AlbumTrackSummaryRepository $albumTrackSummaryRepository; 22 | 23 | public function __construct() 24 | { 25 | $this->albumTrackSummaryRepository = Services::albumTrackSummary(); 26 | } 27 | 28 | public function totalsong(): string 29 | { 30 | $data = []; 31 | $data['summary'] = $this->albumTrackSummaryRepository->findPaginatedSummaryTotalSongData(); 32 | $data['pager'] = model(AlbumModel::class)->pager; 33 | 34 | return view('Album\Views\album-track-summary\totalsong', $data); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Views/album-track-summary/totalsong.php: -------------------------------------------------------------------------------- 1 | setVar('title', $title); 6 | // extends layout 7 | echo $this->extend('Album\Views\layout'); 8 | // begin section content 9 | echo $this->section('content'); 10 | ?> 11 |

14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 38 |
IDTitleArtistTotal Songs
No album track summary found.
id) ?>title) ?>artist) ?>total_song) ?>
39 | 40 | links() ?> 42 | 43 |

44 | 47 | 48 | endSection(); 51 | -------------------------------------------------------------------------------- /test/unit/Domain/AlbumTrackSummary/AlbumTrackSummaryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Unit\Domain\AlbumTrackSummary; 13 | 14 | use Album\Domain\AlbumTrackSummary\AlbumTrackSummary; 15 | use PHPUnit\Framework\TestCase; 16 | 17 | /** 18 | * @internal 19 | */ 20 | final class AlbumTrackSummaryTest extends TestCase 21 | { 22 | public function testFillGetAttributes(): void 23 | { 24 | $albumTrackSummary = new AlbumTrackSummary([ 25 | 'id' => 1, 26 | 'artist' => 'Sheila On 7', 27 | 'title' => 'Kisah Klasik Untuk Masa Depan', 28 | 'total_song' => 1, 29 | ]); 30 | 31 | $this->assertSame(1, $albumTrackSummary->id); 32 | $this->assertSame('Sheila On 7', $albumTrackSummary->artist); 33 | $this->assertSame('Kisah Klasik Untuk Masa Depan', $albumTrackSummary->title); 34 | $this->assertSame(1, $albumTrackSummary->total_song); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Models/TrackModel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Models; 13 | 14 | use Album\Domain\Track\Track; 15 | use CodeIgniter\Database\MySQLi\Connection; 16 | use CodeIgniter\Model; 17 | 18 | /** 19 | * @property Connection $db 20 | */ 21 | final class TrackModel extends Model 22 | { 23 | /** 24 | * @var string 25 | */ 26 | protected $table = 'track'; 27 | 28 | /** 29 | * @var string 30 | */ 31 | protected $returnType = Track::class; 32 | 33 | /** 34 | * @var list 35 | */ 36 | protected $allowedFields = [ 37 | 'album_id', 38 | 'title', 39 | 'author', 40 | ]; 41 | 42 | /** 43 | * @var array 44 | */ 45 | protected $validationRules = [ 46 | 'album_id' => 'required|numeric', 47 | 'title' => 'required|alpha_numeric_space|min_length[3]|max_length[255]', 48 | 'author' => 'required|alpha_numeric_space|min_length[3]|max_length[255]', 49 | ]; 50 | } 51 | -------------------------------------------------------------------------------- /src/Models/AlbumModel.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Models; 13 | 14 | use Album\Domain\Album\Album; 15 | use CodeIgniter\Database\MySQLi\Connection; 16 | use CodeIgniter\Model; 17 | 18 | /** 19 | * @property Connection $db 20 | */ 21 | final class AlbumModel extends Model 22 | { 23 | /** 24 | * @var string 25 | */ 26 | protected $table = 'album'; 27 | 28 | /** 29 | * @var string 30 | */ 31 | protected $returnType = Album::class; 32 | 33 | /** 34 | * @var list 35 | */ 36 | protected $allowedFields = [ 37 | 'artist', 38 | 'title', 39 | ]; 40 | 41 | /** 42 | * @var array 43 | */ 44 | protected $validationRules = [ 45 | 'id' => 'permit_empty|numeric', 46 | 'artist' => 'required|alpha_numeric_space|min_length[3]|max_length[255]', 47 | 'title' => 'required|alpha_numeric_space|min_length[3]|max_length[255]|is_unique[album.title,id,{id}]', 48 | ]; 49 | } 50 | -------------------------------------------------------------------------------- /src/Views/album/add.php: -------------------------------------------------------------------------------- 1 | setVar('title', $title); 6 | // extends layout 7 | echo $this->extend('Album\Views\layout'); 8 | // begin section content 9 | echo $this->section('content'); 10 | ?> 11 | 12 |

14 |

15 | 16 | 20 | 21 |
22 | 'title']); 24 | echo form_input('title', set_value('title'), ['class' => 'form-control']); 25 | ?> 26 | 28 | 29 |
30 | 31 |
32 | 'artist']); 34 | echo form_input('artist', set_value('artist'), ['class' => 'form-control']); 35 | ?> 36 | 38 | 39 |
40 | 41 |
42 | 'btn btn-primary']); 44 | ?> 45 |
46 | 47 | 50 | 51 |

52 | 55 | 56 | endSection(); 59 | -------------------------------------------------------------------------------- /src/Database/Migrations/2020-03-06-003734_Album.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Database\Migrations; 13 | 14 | use CodeIgniter\Database\Migration; 15 | 16 | final class Album extends Migration 17 | { 18 | /** 19 | * @var string 20 | */ 21 | private const TYPE = 'type'; 22 | 23 | public function up(): void 24 | { 25 | $this->forge->addField([ 26 | 'id' => [ 27 | self::TYPE => 'BIGINT', 28 | 'unsigned' => true, 29 | 'auto_increment' => true, 30 | ], 31 | 'artist' => [ 32 | self::TYPE => 'VARCHAR', 33 | 'constraint' => '255', 34 | ], 35 | 'title' => [ 36 | self::TYPE => 'VARCHAR', 37 | 'constraint' => '255', 38 | ], 39 | ]); 40 | $this->forge->addKey('id', true); 41 | $this->forge->createTable('album'); 42 | } 43 | 44 | public function down(): void 45 | { 46 | $this->forge->dropTable('album'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src/Config 7 | src/Controllers 8 | src/Domain 9 | src/Infrastructure 10 | src/Models 11 | src/Views 12 | test 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Views/album/edit.php: -------------------------------------------------------------------------------- 1 | setVar('title', $title); 6 | // extends layout 7 | echo $this->extend('Album\Views\layout'); 8 | // begin section content 9 | echo $this->section('content'); 10 | ?> 11 | 12 |

14 |

15 | 16 | id)); 19 | echo form_hidden('id', set_value('id', $album->id)); 20 | ?> 21 | 22 |
23 | 'title']); 25 | echo form_input('title', set_value('title', $album->title), ['class' => 'form-control']); 26 | ?> 27 | 29 | 30 |
31 | 32 |
33 | 'artist']); 35 | echo form_input('artist', set_value('artist', $album->artist), ['class' => 'form-control']); 36 | ?> 37 | 39 | 40 |
41 | 42 |
43 | 'btn btn-primary']); 45 | ?> 46 |
47 | 50 | 51 |

52 | 55 | 56 | endSection(); 59 | -------------------------------------------------------------------------------- /src/Views/track/add.php: -------------------------------------------------------------------------------- 1 | artist, $album->title); 5 | $this->setVar('title', $title); 6 | // extends layout 7 | echo $this->extend('Album\Views\layout'); 8 | // begin section content 9 | echo $this->section('content'); 10 | ?> 11 | 12 |

14 |

15 | 16 | id)); 19 | echo form_hidden('album_id', set_value('album_id', $album->id)); 20 | ?> 21 | 22 |
23 | 'title']); 25 | echo form_input('title', set_value('title'), ['class' => 'form-control']); 26 | ?> 27 | 29 | 30 |
31 | 32 |
33 | 'author']); 35 | echo form_input('author', set_value('author'), ['class' => 'form-control']); 36 | ?> 37 | 39 | 40 |
41 | 42 |
43 | 'btn btn-primary']); 45 | ?> 46 | 47 | 50 | 51 |

52 | id), 55 | sprintf('Back to Track Index of %s:%s', $album->artist, $album->title) 56 | ); 57 | ?> 58 | 59 | endSection(); 62 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/AlbumTrackSummary/SQLAlbumTrackSummaryRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Infrastructure\Persistence\AlbumTrackSummary; 13 | 14 | use Album\Config\Album; 15 | use Album\Domain\AlbumTrackSummary\AlbumTrackSummary; 16 | use Album\Domain\AlbumTrackSummary\AlbumTrackSummaryRepository; 17 | use Album\Models\AlbumModel; 18 | use Album\Models\TrackModel; 19 | 20 | final readonly class SQLAlbumTrackSummaryRepository implements AlbumTrackSummaryRepository 21 | { 22 | public function __construct(private AlbumModel $albumModel, private TrackModel $trackModel) 23 | { 24 | } 25 | 26 | public function findPaginatedSummaryTotalSongData(): ?array 27 | { 28 | $this->albumModel->builder() 29 | ->select([ 30 | '*', 31 | '(' . $this->trackModel 32 | ->builder() 33 | ->select('count(*)') 34 | ->where('album_id = album.id') 35 | ->getCompiledSelect() . 36 | ') AS total_song', 37 | ]); 38 | $this->albumModel->asObject(AlbumTrackSummary::class); 39 | 40 | /** @var Album $album */ 41 | $album = config('Album'); 42 | 43 | return $this->albumModel 44 | ->paginate($album->paginationPerPage); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/Database/Infrastructure/Persistence/AlbumTrackSummary/SQLAlbumTrackSummaryRepositoryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Database\Infrastructure\Persistence\Album; 13 | 14 | use Album\Database\Seeds\AlbumSeeder; 15 | use Album\Database\Seeds\TrackSeeder; 16 | use CodeIgniter\Test\CIUnitTestCase; 17 | use CodeIgniter\Test\DatabaseTestTrait; 18 | use Config\Services; 19 | 20 | /** 21 | * @internal 22 | */ 23 | final class SQLAlbumTrackSummaryRepositoryTest extends CIUnitTestCase 24 | { 25 | use DatabaseTestTrait; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $basePath = __DIR__ . '/../src/Database/'; 31 | 32 | /** 33 | * @var string 34 | */ 35 | protected $namespace = 'Album'; 36 | 37 | /** 38 | * @var list 39 | */ 40 | protected $seed = [ 41 | AlbumSeeder::class, 42 | TrackSeeder::class, 43 | ]; 44 | 45 | private $repository; 46 | 47 | protected function setUp(): void 48 | { 49 | parent::setUp(); 50 | 51 | $this->repository = Services::albumTrackSummary(); 52 | } 53 | 54 | public function testFindPaginatedSummaryTotalSongDataFoundInDB(): void 55 | { 56 | $albumtracksummary = $this->repository->findPaginatedSummaryTotalSongData(); 57 | $this->assertNotEmpty($albumtracksummary); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Database/Migrations/2020-03-26-203957_Track.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Database\Migrations; 13 | 14 | use CodeIgniter\Database\Migration; 15 | 16 | final class Track extends Migration 17 | { 18 | /** 19 | * @var string 20 | */ 21 | private const ID = 'id'; 22 | 23 | /** 24 | * @var string 25 | */ 26 | private const TYPE = 'type'; 27 | 28 | public function up(): void 29 | { 30 | $this->forge->addField([ 31 | self::ID => [ 32 | self::TYPE => 'BIGINT', 33 | 'unsigned' => true, 34 | 'auto_increment' => true, 35 | ], 36 | 'album_id' => [ 37 | self::TYPE => 'BIGINT', 38 | 'unsigned' => true, 39 | ], 40 | 'title' => [ 41 | self::TYPE => 'VARCHAR', 42 | 'constraint' => '255', 43 | ], 44 | 'author' => [ 45 | self::TYPE => 'VARCHAR', 46 | 'constraint' => '255', 47 | ], 48 | ]); 49 | $this->forge->addKey(self::ID, true); 50 | $this->forge->addForeignKey('album_id', 'album', self::ID, 'CASCADE', 'CASCADE'); 51 | $this->forge->createTable('track'); 52 | } 53 | 54 | public function down(): void 55 | { 56 | $this->forge->dropTable('track'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Views/track/edit.php: -------------------------------------------------------------------------------- 1 | artist, $album->title); 5 | $this->setVar('title', $title); 6 | // extends layout 7 | echo $this->extend('Album\Views\layout'); 8 | // begin section content 9 | echo $this->section('content'); 10 | ?> 11 | 12 |

14 |

15 | 16 | id, $track->id)); 19 | echo form_hidden('album_id', set_value('album_id', $album->id)); 20 | echo form_hidden('id', set_value('id', $track->id)); 21 | ?> 22 | 23 | 24 |
25 | 'title']); 27 | echo form_input('title', set_value('title', $track->title), ['class' => 'form-control']); 28 | ?> 29 | 31 | 32 |
33 | 34 |
35 | 'author']); 37 | echo form_input('author', set_value('author', $track->author), ['class' => 'form-control']); 38 | ?> 39 | 41 | 42 |
43 | 44 |
45 | 'btn btn-primary']); 47 | ?> 48 |
49 | 50 | 53 | 54 |

55 | id), 58 | sprintf('Back to Track Index of %s:%s', $album->artist, $album->title) 59 | ); 60 | ?> 61 | 62 | endSection(); 65 | -------------------------------------------------------------------------------- /src/Config/Services.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Config; 13 | 14 | use Album\Infrastructure\Persistence\Album\SQLAlbumRepository; 15 | use Album\Infrastructure\Persistence\AlbumTrackSummary\SQLAlbumTrackSummaryRepository; 16 | use Album\Infrastructure\Persistence\Track\SQLTrackRepository; 17 | use Album\Models\AlbumModel; 18 | use Album\Models\TrackModel; 19 | use CodeIgniter\Config\BaseService; 20 | 21 | final class Services extends BaseService 22 | { 23 | public static function albumRepository(bool $getShared = true): SQLAlbumRepository 24 | { 25 | if ($getShared) { 26 | return self::getSharedInstance('albumRepository'); 27 | } 28 | 29 | return new SQLAlbumRepository(model(AlbumModel::class)); 30 | } 31 | 32 | public static function trackRepository(bool $getShared = true): SQLTrackRepository 33 | { 34 | if ($getShared) { 35 | return self::getSharedInstance('trackRepository'); 36 | } 37 | 38 | return new SQLTrackRepository(model(TrackModel::class)); 39 | } 40 | 41 | public static function albumTrackSummary(bool $getShared = true): SQLAlbumTrackSummaryRepository 42 | { 43 | if ($getShared) { 44 | return self::getSharedInstance('albumTrackSummary'); 45 | } 46 | 47 | return new SQLAlbumTrackSummaryRepository( 48 | model(AlbumModel::class), 49 | model(TrackModel::class), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Config/Routes.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Config; 13 | 14 | $routes->group('album', ['namespace' => 'Album\Controllers'], static function ($routes): void { 15 | // URI: /album 16 | $routes->get('', 'Album::index', ['as' => 'album-index']); 17 | 18 | // URI: /album/add 19 | $routes->match(['get', 'post'], 'add', 'Album::add', ['as' => 'album-add']); 20 | 21 | // example URI: /album/delete/1 22 | $routes->get('delete/(:num)', 'Album::delete/$1', ['as' => 'album-delete']); 23 | 24 | // example URI: /album/1 25 | $routes->match(['get', 'post'], 'edit/(:num)', 'Album::edit/$1', ['as' => 'album-edit']); 26 | }); 27 | 28 | $routes->group('album-track', ['namespace' => 'Album\Controllers'], static function ($routes): void { 29 | // URI: /track/1 30 | $routes->get('(:num)', 'Track::index/$1', ['as' => 'track-index']); 31 | 32 | // URI: /track/add/(:num) 33 | $routes->match(['get', 'post'], 'add/(:num)', 'Track::add/$1', ['as' => 'track-add']); 34 | 35 | // example URI: /track/delete/1 36 | $routes->get('delete/(:num)/(:num)', 'Track::delete/$1/$2', ['as' => 'track-delete']); 37 | 38 | // example URI: /track/1 39 | $routes->match(['get', 'post'], 'edit/(:num)/(:num)', 'Track::edit/$1/$2', ['as' => 'track-edit']); 40 | }); 41 | 42 | $routes->group('album-track-summary', ['namespace' => 'Album\Controllers'], static function ($routes): void { 43 | // URI: /album-track-summary/totalsong 44 | $routes->get('totalsong', 'AlbumTrackSummary::totalsong', ['as' => 'album-track-summary-totalsong']); 45 | }); 46 | -------------------------------------------------------------------------------- /src/Views/track/index.php: -------------------------------------------------------------------------------- 1 | artist, $album->title); 5 | $this->setVar('title', $title); 6 | // extends layout 7 | echo $this->extend('Album\Views\layout'); 8 | // begin section content 9 | echo $this->section('content'); 10 | ?> 11 |

13 |

14 |

15 | 18 |  |  19 | id), 'Add new album track'); 21 | ?> 22 |

23 | 24 | id), ['method' => 'get']); 27 | echo form_input('keyword', esc($keyword), ['placeholder' => 'Search keyword']); 28 | echo form_close(); 29 | ?> 30 | 31 |
32 | getFlashdata('status'); 34 | ?> 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 62 | 63 | 66 |
TitleAuthorOptions
No album track found.
title) ?>author) ?> 54 | id, $track->id), 'Edit'); ?> |  55 | id, $track->id), 57 | 'Delete', 58 | ['onclick' => "return confirm('Are you sure?')"] 59 | ); 60 | ?> 61 |
67 | 68 | links() ?> 70 | 71 | endSection(); 74 | -------------------------------------------------------------------------------- /src/Views/album/index.php: -------------------------------------------------------------------------------- 1 | setVar('title', $title); 6 | // extends layout 7 | echo $this->extend('Album\Views\layout'); 8 | // begin section content 9 | echo $this->section('content'); 10 | ?> 11 |

14 |

15 | 18 |  |  19 | 22 |

23 | 24 | 'get']); 27 | echo form_input('keyword', esc($keyword), ['placeholder' => 'Search keyword']); 28 | echo form_close(); 29 | ?> 30 | 31 |
32 | getFlashdata('status'); 34 | ?> 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 63 | 64 | 67 |
TitleArtistOptions
No album found.
title) ?>artist) ?> 54 | id), 'Album Tracks Details'); ?>  |  55 | id), 'Edit'); ?>  |  56 | id), 58 | 'Delete', 59 | ['onclick' => "return confirm('Track records will also deleted, are you sure?')"] 60 | ); 61 | ?> 62 |
68 | links() ?> 70 | 71 | 72 | endSection(); 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samsonasik/ci4-album", 3 | "type": "library", 4 | "description": "An Example of CodeIgniter 4 Album module", 5 | "keywords": [ 6 | "ci4", 7 | "codeigniter4", 8 | "codeigniter" 9 | ], 10 | "homepage": "https://github.com/samsonasik/ci4-album", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Abdul Malik Ikhsan", 15 | "email": "samsonasik@gmail.com", 16 | "homepage": "http://samsonasik.wordpress.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "~8.2.0 || ~8.3.0 || ~8.4.0" 22 | }, 23 | "conflict": { 24 | "codeigniter4/framework": "<4.6" 25 | }, 26 | "require-dev": { 27 | "codeigniter/coding-standard": "^1.8.2", 28 | "codeigniter4/framework": "^4.6", 29 | "phpstan/phpstan": "^2.0.4", 30 | "phpunit/phpunit": "^11.5.2", 31 | "rector/rector": "dev-main" 32 | }, 33 | "config": { 34 | "sort-packages": true, 35 | "allow-plugins": { 36 | "infection/extension-installer": true 37 | } 38 | }, 39 | "autoload": { 40 | "psr-4": { 41 | "Album\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "AlbumTest\\": "test/", 47 | "CodeIgniter4\\": "vendor/codeigniter4/codeigniter4-standard/CodeIgniter4/" 48 | }, 49 | "classmap": [ 50 | "vendor/codeigniter4/framework/app/Controllers/BaseController.php" 51 | ] 52 | }, 53 | "minimum-stability": "dev", 54 | "prefer-stable": true, 55 | "scripts": { 56 | "analyze": "phpstan analyze src --level=max", 57 | "rectify": "rector --dry-run", 58 | "cs-check": "php-cs-fixer fix --dry-run --diff", 59 | "cs-fix": "php-cs-fixer fix", 60 | "test": "phpunit --colors=always" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ./test 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ./src 36 | 37 | 38 | ./src/Config 39 | ./src/Database 40 | ./src/Views 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Album/SQLAlbumRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Infrastructure\Persistence\Album; 13 | 14 | use Album\Config\Album as ConfigAlbum; 15 | use Album\Domain\Album\Album; 16 | use Album\Domain\Album\AlbumNotFoundException; 17 | use Album\Domain\Album\AlbumRepository; 18 | use Album\Infrastructure\Persistence\DMLPersistence; 19 | use Album\Models\AlbumModel; 20 | 21 | final readonly class SQLAlbumRepository implements AlbumRepository 22 | { 23 | use DMLPersistence; 24 | 25 | public function __construct(private readonly AlbumModel $model) 26 | { 27 | } 28 | 29 | public function findPaginatedData(string $keyword = ''): ?array 30 | { 31 | if ($keyword !== '') { 32 | $this->model 33 | ->builder() 34 | ->groupStart() 35 | ->like('artist', $keyword) 36 | ->orLike('title', $keyword) 37 | ->groupEnd(); 38 | } 39 | 40 | /** @var ConfigAlbum $album */ 41 | $album = config('Album'); 42 | 43 | return $this->model->paginate($album->paginationPerPage); 44 | } 45 | 46 | public function findAlbumOfId(int $id): Album 47 | { 48 | $album = $this->model->find($id); 49 | if (! $album instanceof Album) { 50 | throw AlbumNotFoundException::forAlbumDoesnotExistOfId($id); 51 | } 52 | 53 | return $album; 54 | } 55 | 56 | public function deleteOfId(int $id): bool 57 | { 58 | $this->model->delete($id); 59 | if ($this->model->db->affectedRows() === 0) { 60 | throw AlbumNotFoundException::forAlbumDoesnotExistOfId($id); 61 | } 62 | 63 | return true; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /phpunit.xml.github-actions.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ./test 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ./src 36 | 37 | 38 | ./src/Config 39 | ./src/Database 40 | ./src/Views 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/ci_build.yaml: -------------------------------------------------------------------------------- 1 | name: "ci build" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - "master" 8 | 9 | jobs: 10 | build: 11 | name: PHP ${{ matrix.php-versions }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | php-versions: ['8.2', '8.3', '8.4'] 17 | steps: 18 | - name: Setup PHP Action 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | extensions: intl 22 | php-version: "${{ matrix.php-versions }}" 23 | coverage: xdebug 24 | - name: Setup MySQL 25 | uses: shogo82148/actions-setup-mysql@v1 26 | with: 27 | mysql-version: '8.0' 28 | - run: mysql -uroot -h127.0.0.1 -e 'SELECT version()' 29 | - name: Checkout 30 | uses: actions/checkout@v2 31 | - name: Create DB 32 | run: "mysql -u root -h 127.0.0.1 -e 'create database ci4_crud_test'" 33 | - name: "Validate composer.json and composer.lock" 34 | run: "composer validate" 35 | - name: "Install dependencies" 36 | run: "composer install" 37 | - name: "CS Check" 38 | run: "export PHP_CS_FIXER_IGNORE_ENV=true && composer cs-check" 39 | - name: "Code analyze" 40 | run: | 41 | composer analyze 42 | composer rectify 43 | - name: "Run test suite" 44 | run: "mv phpunit.xml.github-actions.dist phpunit.xml.dist && composer test" 45 | - if: matrix.php-versions == '8.2' 46 | name: Run mutation test 47 | env: 48 | INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} 49 | STRYKER_DASHBOARD_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} 50 | run: | 51 | composer require --dev infection/infection 52 | vendor/bin/infection 53 | - if: matrix.php-versions == '8.2' 54 | name: Upload coverage to Codecov 55 | uses: codecov/codecov-action@v1 56 | with: 57 | token: ${{ secrets.CODECOV_TOKEN }} 58 | file: ./build/logs/clover.xml 59 | flags: tests 60 | name: codecov-umbrella 61 | yml: ./codecov.yml 62 | fail_ci_if_error: true 63 | -------------------------------------------------------------------------------- /test/Controller/AlbumTrackSummaryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Controller; 13 | 14 | use Album\Controllers\AlbumTrackSummary; 15 | use Album\Database\Seeds\AlbumSeeder; 16 | use Album\Database\Seeds\TrackSeeder; 17 | use CodeIgniter\Test\CIUnitTestCase; 18 | use CodeIgniter\Test\ControllerTestTrait; 19 | use CodeIgniter\Test\DatabaseTestTrait; 20 | use Config\Database; 21 | use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; 22 | 23 | #[RunTestsInSeparateProcesses] 24 | /** 25 | * @internal 26 | */ 27 | final class AlbumTrackSummaryTest extends CIUnitTestCase 28 | { 29 | use ControllerTestTrait; 30 | use DatabaseTestTrait; 31 | 32 | /** 33 | * @var string 34 | */ 35 | protected $basePath = __DIR__ . '/../src/Database/'; 36 | 37 | /** 38 | * @var string 39 | */ 40 | protected $namespace = 'Album'; 41 | 42 | /** 43 | * @var list 44 | */ 45 | protected $seed = [ 46 | AlbumSeeder::class, 47 | TrackSeeder::class, 48 | ]; 49 | 50 | public function testTotalSongSummaryHasNoData(): void 51 | { 52 | Database::connect()->disableForeignKeyChecks(); 53 | Database::connect()->table('album')->truncate(); 54 | Database::connect()->enableForeignKeyChecks(); 55 | 56 | $testResponse = $this->controller(AlbumTrackSummary::class) 57 | ->execute('totalsong'); 58 | 59 | $this->assertTrue($testResponse->isOK()); 60 | $this->assertTrue($testResponse->see('No album track summary found.')); 61 | } 62 | 63 | public function testTotalSongSummaryHasData(): void 64 | { 65 | $testResponse = $this->controller(AlbumTrackSummary::class) 66 | ->execute('totalsong'); 67 | 68 | $this->assertTrue($testResponse->isOK()); 69 | $this->assertMatchesRegularExpression('/Sheila On 7<\/td>\s{0,}\n\s{0,}1<\/td>/', $testResponse->getBody()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Infrastructure/Persistence/Track/SQLTrackRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Infrastructure\Persistence\Track; 13 | 14 | use Album\Config\Album as ConfigAlbum; 15 | use Album\Domain\Album\Album; 16 | use Album\Domain\Track\Track; 17 | use Album\Domain\Track\TrackDuplicatedRectorException; 18 | use Album\Domain\Track\TrackNotFoundException; 19 | use Album\Domain\Track\TrackRepository; 20 | use Album\Infrastructure\Persistence\DMLPersistence; 21 | use Album\Models\TrackModel; 22 | use Config\Services; 23 | 24 | final readonly class SQLTrackRepository implements TrackRepository 25 | { 26 | use DMLPersistence { 27 | save as saveData; 28 | } 29 | 30 | public function __construct(private readonly TrackModel $model) 31 | { 32 | } 33 | 34 | public function findPaginatedData(Album $album, string $keyword = ''): ?array 35 | { 36 | $this->model 37 | ->builder() 38 | ->where('album_id', $album->id); 39 | 40 | if ($keyword !== '') { 41 | $this->model 42 | ->builder() 43 | ->groupStart() 44 | ->like('title', $keyword) 45 | ->orLike('author', $keyword) 46 | ->groupEnd(); 47 | } 48 | 49 | /** @var ConfigAlbum $album */ 50 | $album = config('Album'); 51 | 52 | return $this->model->paginate($album->paginationPerPage); 53 | } 54 | 55 | public function findTrackOfId(int $id): Track 56 | { 57 | $track = $this->model->find($id); 58 | if (! $track instanceof Track) { 59 | throw TrackNotFoundException::forAlbumTrackDoesnotExistOfId($id); 60 | } 61 | 62 | return $track; 63 | } 64 | 65 | public function deleteOfId(int $id): bool 66 | { 67 | $this->model->delete($id); 68 | if ($this->model->db->affectedRows() === 0) { 69 | throw TrackNotFoundException::forAlbumTrackDoesnotExistOfId($id); 70 | } 71 | 72 | return true; 73 | } 74 | 75 | public function save(?array $data = null): bool 76 | { 77 | // model->validate() check empty data early to run true 78 | // which on controller test with invalid data, it bypassed 79 | // so need to instantiate validation service with set rules here 80 | $validation = Services::validation(null, false); 81 | $isValid = $validation->setRules($this->model->getValidationRules()) 82 | ->run($data); 83 | 84 | if (! $isValid) { 85 | return false; 86 | } 87 | 88 | if (isset($data['id'])) { 89 | /** @var array{id: int, title: string, album_id:int} $data */ 90 | $this->model 91 | ->builder() 92 | ->where('id !=', $data['id']) 93 | ->where('title', $data['title']) 94 | ->where('album_id', $data['album_id']); 95 | 96 | $result = $this->model->get()->getResult(); 97 | if ($result === []) { 98 | return $this->saveData($data); 99 | } 100 | 101 | throw TrackDuplicatedRectorException::forDuplicatedTitle($data['album_id']); 102 | } 103 | 104 | /** @var array{title: string, album_id:int} $data */ 105 | $this->model 106 | ->builder() 107 | ->where('album_id', $data['album_id']) 108 | ->where('title', $data['title']); 109 | 110 | $result = $this->model->get()->getResult(); 111 | if ($result !== []) { 112 | throw TrackDuplicatedRectorException::forDuplicatedTitle($data['album_id']); 113 | } 114 | 115 | return $this->saveData($data); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Controllers/Album.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Controllers; 13 | 14 | use Album\Config\Services; 15 | use Album\Domain\Album\AlbumRepository; 16 | use Album\Domain\Exception\RecordNotFoundException; 17 | use Album\Models\AlbumModel; 18 | use App\Controllers\BaseController; 19 | use CodeIgniter\Exceptions\PageNotFoundException; 20 | use CodeIgniter\HTTP\IncomingRequest; 21 | use CodeIgniter\HTTP\RedirectResponse; 22 | 23 | final class Album extends BaseController 24 | { 25 | /** 26 | * @var string 27 | */ 28 | private const KEYWORD = 'keyword'; 29 | 30 | /** 31 | * @var string 32 | */ 33 | private const STATUS = 'status'; 34 | 35 | /** 36 | * @var string 37 | */ 38 | private const ALBUM_INDEX = 'album-index'; 39 | 40 | /** 41 | * @var string 42 | */ 43 | private const ERRORS = 'errors'; 44 | 45 | /** 46 | * @var IncomingRequest 47 | */ 48 | protected $request; 49 | 50 | private readonly AlbumRepository $albumRepository; 51 | 52 | public function __construct() 53 | { 54 | $this->albumRepository = Services::albumRepository(); 55 | } 56 | 57 | public function index(): string 58 | { 59 | $data = []; 60 | /** @var string $keyword */ 61 | $keyword = $this->request->getGet(self::KEYWORD) ?? ''; 62 | $data[self::KEYWORD] = $keyword; 63 | $data['albums'] = $this->albumRepository->findPaginatedData($keyword); 64 | $data['pager'] = model(AlbumModel::class)->pager; 65 | 66 | return view('Album\Views\album\index', $data); 67 | } 68 | 69 | public function add(): RedirectResponse|string 70 | { 71 | if ($this->request->getMethod() === 'post') { 72 | /** @var array $post */ 73 | $post = $this->request->getPost(); 74 | if ($this->albumRepository->save($post)) { 75 | session()->setFlashdata(self::STATUS, 'New album has been added'); 76 | 77 | return redirect()->route(self::ALBUM_INDEX); 78 | } 79 | 80 | session()->setFlashdata(self::ERRORS, model(AlbumModel::class)->errors()); 81 | 82 | return redirect()->withInput()->back(); 83 | } 84 | 85 | return view('Album\Views\album\add', [self::ERRORS => session()->getFlashData(self::ERRORS)]); 86 | } 87 | 88 | public function edit(int $id): RedirectResponse|string 89 | { 90 | try { 91 | $album = $this->albumRepository->findAlbumOfId($id); 92 | } catch (RecordNotFoundException $recordNotFoundException) { 93 | throw PageNotFoundException::forPageNotFound($recordNotFoundException->getMessage()); 94 | } 95 | 96 | if ($this->request->getMethod() === 'post') { 97 | /** @var array $post */ 98 | $post = $this->request->getPost(); 99 | if ($this->albumRepository->save($post)) { 100 | session()->setFlashdata(self::STATUS, 'Album has been updated'); 101 | 102 | return redirect()->route(self::ALBUM_INDEX); 103 | } 104 | 105 | session()->setFlashdata(self::ERRORS, model(AlbumModel::class)->errors()); 106 | 107 | return redirect()->withInput()->back(); 108 | } 109 | 110 | return view('Album\Views\album\edit', ['album' => $album, self::ERRORS => session()->getFlashData(self::ERRORS)]); 111 | } 112 | 113 | public function delete(int $id): RedirectResponse 114 | { 115 | try { 116 | $this->albumRepository->deleteOfId($id); 117 | } catch (RecordNotFoundException $recordNotFoundException) { 118 | throw PageNotFoundException::forPageNotFound($recordNotFoundException->getMessage()); 119 | } 120 | 121 | session()->setFlashdata(self::STATUS, 'Album has been deleted'); 122 | 123 | return redirect()->route(self::ALBUM_INDEX); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /test/Database/Infrastructure/Persistence/Album/SQLAlbumRepositoryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Database\Infrastructure\Persistence\Album; 13 | 14 | use Album\Database\Seeds\AlbumSeeder; 15 | use Album\Domain\Album\Album; 16 | use Album\Domain\Album\AlbumNotFoundException; 17 | use CodeIgniter\Test\CIUnitTestCase; 18 | use CodeIgniter\Test\DatabaseTestTrait; 19 | use Config\Services; 20 | use PHPUnit\Framework\Attributes\DataProvider; 21 | use PHPUnit\Framework\Attributes\PreserveGlobalState; 22 | use PHPUnit\Framework\Attributes\RunInSeparateProcess; 23 | 24 | /** 25 | * @internal 26 | */ 27 | final class SQLAlbumRepositoryTest extends CIUnitTestCase 28 | { 29 | use DatabaseTestTrait; 30 | 31 | /** 32 | * @var string 33 | */ 34 | protected $basePath = __DIR__ . '/../src/Database/'; 35 | 36 | /** 37 | * @var string 38 | */ 39 | protected $namespace = 'Album'; 40 | 41 | /** 42 | * @var string 43 | */ 44 | protected $seed = AlbumSeeder::class; 45 | 46 | private $repository; 47 | 48 | protected function setUp(): void 49 | { 50 | parent::setUp(); 51 | 52 | $this->repository = Services::albumRepository(); 53 | } 54 | 55 | public function testfindPaginatedDataWithKeywordNotFoundInDB(): void 56 | { 57 | $albums = $this->repository->findPaginatedData('Siti'); 58 | $this->assertEmpty($albums); 59 | } 60 | 61 | public function testfindPaginatedDataWithKeywordFoundInDB(): void 62 | { 63 | $albums = $this->repository->findPaginatedData('Sheila'); 64 | $this->assertNotEmpty($albums); 65 | } 66 | 67 | public function testFindAlbumOfIdWithNotFoundIdInDB(): void 68 | { 69 | $this->expectException(AlbumNotFoundException::class); 70 | $this->repository->findAlbumOfId(random_int(1000, 2000)); 71 | } 72 | 73 | public function testFindAlbumOfIdWithFoundIdInDatabase(): void 74 | { 75 | $this->assertInstanceOf(Album::class, $this->repository->findAlbumOfId(1)); 76 | } 77 | 78 | /** 79 | * @return array> 80 | */ 81 | public static function invalidData(): array 82 | { 83 | return [ 84 | 'empty array' => [ 85 | [], 86 | ], 87 | 'null' => [ 88 | null, 89 | ], 90 | ]; 91 | } 92 | 93 | /** 94 | * @param list|null $data 95 | */ 96 | #[DataProvider('invalidData')] 97 | public function testSaveInvalidData(?array $data): void 98 | { 99 | $this->assertFalse($this->repository->save($data)); 100 | } 101 | 102 | /** 103 | * @return array{insert: array, update: array} 104 | */ 105 | public static function validData(): array 106 | { 107 | return [ 108 | 'insert' => [ 109 | [ 110 | 'artist' => 'Siti Nurhaliza', 111 | 'title' => 'Anugrah Aidilfitri', 112 | ], 113 | ], 114 | 'update' => [ 115 | [ 116 | 'id' => 1, 117 | 'artist' => 'Sheila On 7', 118 | 'title' => 'Pejantan Tangguh', 119 | ], 120 | ], 121 | ]; 122 | } 123 | 124 | /** 125 | * @param array|array $data 126 | */ 127 | #[DataProvider('validData')] 128 | #[PreserveGlobalState(false)] 129 | #[RunInSeparateProcess] 130 | public function testSaveValidData(array $data): void 131 | { 132 | $this->assertTrue($this->repository->save($data)); 133 | } 134 | 135 | public function testDeleteAlbumOfIdWithNotFoundIdInDatabase(): void 136 | { 137 | $this->expectException(AlbumNotFoundException::class); 138 | $this->repository->deleteOfId(random_int(1000, 2000)); 139 | } 140 | 141 | public function testDeleteAlbumOfIdWithFoundIdInDatabase(): void 142 | { 143 | $this->assertTrue($this->repository->deleteOfId(1)); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Example of CodeIgniter 4 Module : Album Module 2 | ============================================== 3 | 4 | [![Latest Version](https://img.shields.io/github/release/samsonasik/ci4-album.svg?style=flat-square)](https://github.com/samsonasik/ci4-album/releases) 5 | ![ci build](https://github.com/samsonasik/ci4-album/workflows/ci%20build/badge.svg) 6 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fsamsonasik%2Fci4-album%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/samsonasik/ci4-album/master) 7 | [![Code Coverage](https://codecov.io/gh/samsonasik/ci4-album/branch/master/graph/badge.svg)](https://codecov.io/gh/samsonasik/ci4-album) 8 | [![PHPStan](https://img.shields.io/badge/style-level%20max-brightgreen.svg?style=flat-square&label=phpstan)](https://github.com/phpstan/phpstan) 9 | [![Downloads](https://poser.pugx.org/samsonasik/ci4-album/downloads)](https://packagist.org/packages/samsonasik/ci4-album) 10 | 11 | Feature 12 | ------- 13 | 14 | - [x] CRUD with 15 | - [x] [Domain Driven Design Architecture](https://en.wikipedia.org/wiki/Domain-driven_design) with [Tactical Pattern](http://gorodinski.com/blog/2012/04/25/read-models-as-a-tactical-pattern-in-domain-driven-design-ddd/) 16 | - [x] [Post/Redirect/Get pattern](https://en.wikipedia.org/wiki/Post/Redirect/Get) 17 | - [x] Pagination, configurable via [`Album\Config\Album`](#settings) class. 18 | - [x] Search 19 | - [x] Layout 20 | - [x] Flash Message after add/edit/delete 21 | 22 | Installation 23 | ------------ 24 | 25 | **1.** Get The Module 26 | 27 | **a.** require via composer 28 | 29 | ```bash 30 | composer require samsonasik/ci4-album 31 | ``` 32 | 33 | **OR** 34 | 35 | **b.** manually, by go to `app/ThirdParty` directory in project root, and clone this repository to the `app/ThirdParty` directory: 36 | 37 | ```bash 38 | cd app/ThirdParty 39 | git clone git@github.com:samsonasik/ci4-album.git 40 | ``` 41 | 42 | > see https://help.github.com/en/github/authenticating-to-github/error-permission-denied-publickey# for common clone issue troubleshooting. 43 | 44 | then register "Album" to `App/Config/Autoload.php`'s psr4 property: 45 | 46 | ```php 47 | $psr4 = [ 48 | 'App' => APPPATH, // To ensure filters, etc still found, 49 | APP_NAMESPACE => APPPATH, // For custom namespace 50 | 'Config' => APPPATH . 'Config', 51 | 'Album' => APPPATH . 'ThirdParty/ci4-album/src', // <-- add this line 52 | ]; 53 | ``` 54 | 55 | **2.** Set CI_ENVIRONMENT, base url, index page, and database config in your `.env` file based on your existing database (If you don't have a `.env` file, you can copy first from `env` file: `cp env .env` first). If the database not exists, create database first. 56 | 57 | ```bash 58 | # .env file 59 | CI_ENVIRONMENT = development 60 | 61 | app.baseURL = 'http://localhost:8080' 62 | app.indexPage = '' 63 | 64 | database.default.hostname = localhost 65 | database.default.database = ci4_crud 66 | database.default.username = root 67 | database.default.password = 68 | database.default.DBDriver = MySQLi 69 | ``` 70 | 71 | **3.** Run db migration 72 | 73 | ```bash 74 | php spark migrate -n Album 75 | ``` 76 | 77 | **4.** Run db seed (Optional) 78 | 79 | ```bash 80 | php spark db:seed "Album\Database\Seeds\AlbumSeeder" 81 | php spark db:seed "Album\Database\Seeds\TrackSeeder" 82 | ``` 83 | 84 | **5.** Run development server: 85 | 86 | ```bash 87 | php spark serve 88 | ``` 89 | 90 | **6.** Open in browser http://localhost:8080/album 91 | 92 | Settings 93 | -------- 94 | 95 | Configure pagination per-page, by copy `src/Config/Album.php` file into `app/Config` directory, and modify the namespace to `Config`: 96 | 97 | ```php 98 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | ``` 137 | 138 | > Ensure that you use **different DB** for `testing`. 139 | 140 | 141 | After it, install the codeigniter and phpunit dependency: 142 | 143 | ```bash 144 | cd /path/to/modules/ci4-album && composer install 145 | ``` 146 | 147 | Lastly, run the test: 148 | 149 | ```bash 150 | vendor/bin/phpunit 151 | ```` 152 | 153 | Contributing 154 | ------------ 155 | Contributions are very welcome. Please read [CONTRIBUTING.md](https://github.com/samsonasik/ci4-album/blob/master/CONTRIBUTING.md) 156 | -------------------------------------------------------------------------------- /src/Controllers/Track.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Album\Controllers; 13 | 14 | use Album\Config\Services; 15 | use Album\Domain\Album\AlbumRepository; 16 | use Album\Domain\Exception\DuplicatedRecordException; 17 | use Album\Domain\Exception\RecordNotFoundException; 18 | use Album\Domain\Track\TrackRepository; 19 | use Album\Models\TrackModel; 20 | use App\Controllers\BaseController; 21 | use CodeIgniter\Exceptions\PageNotFoundException; 22 | use CodeIgniter\HTTP\IncomingRequest; 23 | use CodeIgniter\HTTP\RedirectResponse; 24 | 25 | final class Track extends BaseController 26 | { 27 | /** 28 | * @var string 29 | */ 30 | private const KEYWORD = 'keyword'; 31 | 32 | /** 33 | * @var string 34 | */ 35 | private const ALBUM = 'album'; 36 | 37 | /** 38 | * @var string 39 | */ 40 | private const STATUS = 'status'; 41 | 42 | /** 43 | * @var string 44 | */ 45 | private const TRACK_INDEX = 'track-index'; 46 | 47 | /** 48 | * @var string 49 | */ 50 | private const ERRORS = 'errors'; 51 | 52 | /** 53 | * @var IncomingRequest 54 | */ 55 | protected $request; 56 | 57 | private readonly AlbumRepository $albumRepository; 58 | private readonly TrackRepository $trackRepository; 59 | 60 | public function __construct() 61 | { 62 | $this->albumRepository = Services::albumRepository(); 63 | $this->trackRepository = Services::trackRepository(); 64 | } 65 | 66 | public function index(int $albumId): string 67 | { 68 | $data = []; 69 | 70 | try { 71 | $album = $this->albumRepository->findAlbumOfId($albumId); 72 | } catch (RecordNotFoundException $recordNotFoundException) { 73 | throw PageNotFoundException::forPageNotFound($recordNotFoundException->getMessage()); 74 | } 75 | 76 | /** @var string $keyword */ 77 | $keyword = $this->request->getGet(self::KEYWORD) ?? ''; 78 | $data[self::KEYWORD] = $keyword; 79 | $data[self::ALBUM] = $album; 80 | $data['tracks'] = $this->trackRepository->findPaginatedData($album, $keyword); 81 | $data['pager'] = model(TrackModel::class)->pager; 82 | 83 | return view('Album\Views\track\index', $data); 84 | } 85 | 86 | public function add(int $albumId): RedirectResponse|string 87 | { 88 | try { 89 | $album = $this->albumRepository->findAlbumOfId($albumId); 90 | } catch (RecordNotFoundException $recordNotFoundException) { 91 | throw PageNotFoundException::forPageNotFound($recordNotFoundException->getMessage()); 92 | } 93 | 94 | if ($this->request->getMethod() === 'post') { 95 | /** @var array $post */ 96 | $post = $this->request->getPost(); 97 | 98 | try { 99 | if ($this->trackRepository->save($post)) { 100 | session()->setFlashdata(self::STATUS, 'New album track has been added'); 101 | 102 | return redirect()->route(self::TRACK_INDEX, [$albumId]); 103 | } 104 | } catch (DuplicatedRecordException $recordNotFoundException) { 105 | session()->setFlashdata(self::ERRORS, ['title' => $recordNotFoundException->getMessage()]); 106 | 107 | return redirect()->withInput()->back(); 108 | } 109 | 110 | session()->setFlashdata(self::ERRORS, model(TrackModel::class)->errors()); 111 | 112 | return redirect()->withInput()->back(); 113 | } 114 | 115 | return view('Album\Views\track\add', [ 116 | self::ALBUM => $album, 117 | self::ERRORS => session()->getFlashData(self::ERRORS), 118 | ]); 119 | } 120 | 121 | public function edit(int $albumId, int $trackId): RedirectResponse|string 122 | { 123 | try { 124 | $album = $this->albumRepository->findAlbumOfId($albumId); 125 | $track = $this->trackRepository->findTrackOfId($trackId); 126 | } catch (RecordNotFoundException $recordNotFoundException) { 127 | throw PageNotFoundException::forPageNotFound($recordNotFoundException->getMessage()); 128 | } 129 | 130 | if ($this->request->getMethod() === 'post') { 131 | /** @var array $post */ 132 | $post = $this->request->getPost(); 133 | 134 | try { 135 | if ($this->trackRepository->save($post)) { 136 | session()->setFlashdata(self::STATUS, 'Album track has been updated'); 137 | 138 | return redirect()->route(self::TRACK_INDEX, [$albumId]); 139 | } 140 | } catch (DuplicatedRecordException $recordNotFoundException) { 141 | session()->setFlashdata(self::ERRORS, ['title' => $recordNotFoundException->getMessage()]); 142 | 143 | return redirect()->withInput()->back(); 144 | } 145 | 146 | session()->setFlashdata(self::ERRORS, model(TrackModel::class)->errors()); 147 | 148 | return redirect()->withInput()->back(); 149 | } 150 | 151 | return view('Album\Views\track\edit', [ 152 | self::ALBUM => $album, 153 | 'track' => $track, 154 | self::ERRORS => session()->getFlashData(self::ERRORS), 155 | ]); 156 | } 157 | 158 | public function delete(int $albumId, int $trackId): RedirectResponse 159 | { 160 | try { 161 | $this->albumRepository->findAlbumOfId($albumId); 162 | $this->trackRepository->deleteOfId($trackId); 163 | } catch (RecordNotFoundException $recordNotFoundException) { 164 | throw PageNotFoundException::forPageNotFound($recordNotFoundException->getMessage()); 165 | } 166 | 167 | session()->setFlashdata(self::STATUS, 'Album track has been deleted'); 168 | 169 | return redirect()->route(self::TRACK_INDEX, [$albumId]); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /test/Controller/AlbumTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Controller; 13 | 14 | use Album\Controllers\Album; 15 | use Album\Database\Seeds\AlbumSeeder; 16 | use CodeIgniter\Test\CIUnitTestCase; 17 | use CodeIgniter\Test\ControllerTestTrait; 18 | use CodeIgniter\Test\DatabaseTestTrait; 19 | use Config\Database; 20 | use Config\Services; 21 | use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; 22 | 23 | #[RunTestsInSeparateProcesses] 24 | /** 25 | * @internal 26 | */ 27 | final class AlbumTest extends CIUnitTestCase 28 | { 29 | use DatabaseTestTrait; 30 | use ControllerTestTrait; 31 | 32 | /** 33 | * @var string 34 | */ 35 | protected $basePath = __DIR__ . '/../src/Database/'; 36 | 37 | /** 38 | * @var string 39 | */ 40 | protected $namespace = 'Album'; 41 | 42 | /** 43 | * @var string 44 | */ 45 | protected $seed = AlbumSeeder::class; 46 | 47 | public function testIndexAlbumHasNoData(): void 48 | { 49 | Database::connect()->disableForeignKeyChecks(); 50 | Database::connect()->table('album')->truncate(); 51 | Database::connect()->enableForeignKeyChecks(); 52 | 53 | $testResponse = $this->controller(Album::class) 54 | ->execute('index'); 55 | 56 | $this->assertTrue($testResponse->isOK()); 57 | $this->assertTrue($testResponse->see('No album found.')); 58 | } 59 | 60 | public function testIndexAlbumHasData(): void 61 | { 62 | $testResponse = $this->controller(Album::class) 63 | ->execute('index'); 64 | 65 | $this->assertTrue($testResponse->isOK()); 66 | $this->assertTrue($testResponse->see('Sheila On 7')); 67 | } 68 | 69 | public function testIndexSearchAlbumFound(): void 70 | { 71 | $request = Services::request(); 72 | $request = $request->withMethod('get'); 73 | $request->setGlobal('get', [ 74 | 'keyword' => 'Sheila', 75 | ]); 76 | 77 | $testResponse = $this->withRequest($request) 78 | ->controller(Album::class) 79 | ->execute('index'); 80 | 81 | $this->assertTrue($testResponse->see('Sheila On 7')); 82 | } 83 | 84 | public function testIndexSearchAlbumNotFound(): void 85 | { 86 | $request = Services::request(); 87 | $request = $request->withMethod('get'); 88 | $request->setGlobal('get', [ 89 | 'keyword' => 'Siti', 90 | ]); 91 | 92 | $testResponse = $this->withRequest($request) 93 | ->controller(Album::class) 94 | ->execute('index'); 95 | 96 | $this->assertTrue($testResponse->see('No album found.')); 97 | } 98 | 99 | public function testAddAlbum(): void 100 | { 101 | $testResponse = $this->controller(Album::class) 102 | ->execute('add'); 103 | 104 | $this->assertTrue($testResponse->isOK()); 105 | } 106 | 107 | public function testAddAlbumInvalidData(): void 108 | { 109 | $request = Services::request(null, false); 110 | $request = $request->withMethod('post'); 111 | 112 | $this->withRequest($request) 113 | ->controller(Album::class) 114 | ->execute('add'); 115 | 116 | $this->seeNumRecords(1, 'album', []); 117 | } 118 | 119 | public function testAddAlbumValidData(): void 120 | { 121 | $request = Services::request(); 122 | $request = $request->withMethod('post'); 123 | $request->setGlobal('post', [ 124 | 'artist' => 'Siti Nurhaliza', 125 | 'title' => 'Anugrah Aidilfitri', 126 | ]); 127 | 128 | $testResponse = $this->withRequest($request) 129 | ->controller(Album::class) 130 | ->execute('add'); 131 | 132 | $this->assertTrue($testResponse->isRedirect()); 133 | } 134 | 135 | public function testEditUnexistenceAlbum(): void 136 | { 137 | $testResponse = $this->controller(Album::class) 138 | ->execute('edit', random_int(1000, 2000)); 139 | 140 | $this->assertSame(404, $testResponse->response()->getStatusCode()); 141 | } 142 | 143 | public function testEditExistenceAlbum(): void 144 | { 145 | $testResponse = $this->controller(Album::class) 146 | ->execute('edit', 1); 147 | 148 | $this->assertTrue($testResponse->isOK()); 149 | } 150 | 151 | public function testEditAlbumInvalidData(): void 152 | { 153 | $request = Services::request(null, false); 154 | $request = $request->withMethod('post'); 155 | 156 | $testResponse = $this->withRequest($request) 157 | ->controller(Album::class) 158 | ->execute('edit', 1); 159 | $this->assertTrue($testResponse->isRedirect()); 160 | $this->assertNotSame('http://localhost:8080/index.php/album', $testResponse->getRedirectUrl()); 161 | } 162 | 163 | public function testEditAlbumValidData(): void 164 | { 165 | $request = Services::request(); 166 | $request = $request->withMethod('post'); 167 | $request->setGlobal('post', [ 168 | 'id' => 1, 169 | 'artist' => 'Siti Nurhaliza', 170 | 'title' => 'Anugrah Aidilfitri', 171 | ]); 172 | 173 | $testResponse = $this->withRequest($request) 174 | ->controller(Album::class) 175 | ->execute('edit', 1); 176 | 177 | $this->assertTrue($testResponse->isRedirect()); 178 | $this->assertSame('http://localhost:8080/index.php/album', $testResponse->getRedirectUrl()); 179 | } 180 | 181 | public function testDeleteUnexistenceAlbum(): void 182 | { 183 | $testResponse = $this->controller(Album::class) 184 | ->execute('delete', random_int(1000, 2000)); 185 | 186 | $this->assertSame(404, $testResponse->response()->getStatusCode()); 187 | } 188 | 189 | public function testDeleteExistenceAlbum(): void 190 | { 191 | $testResponse = $this->controller(Album::class) 192 | ->execute('delete', 1); 193 | 194 | $this->assertTrue($testResponse->isRedirect()); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /test/Database/Infrastructure/Persistence/Track/SQLTrackRepositoryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Database\Infrastructure\Persistence\Album; 13 | 14 | use Album\Database\Seeds\AlbumSeeder; 15 | use Album\Database\Seeds\TrackSeeder; 16 | use Album\Domain\Album\Album; 17 | use Album\Domain\Exception\DuplicatedRecordException; 18 | use Album\Domain\Track\Track; 19 | use Album\Domain\Track\TrackNotFoundException; 20 | use CodeIgniter\Test\CIUnitTestCase; 21 | use CodeIgniter\Test\DatabaseTestTrait; 22 | use Config\Database; 23 | use Config\Services; 24 | use PHPUnit\Framework\Attributes\DataProvider; 25 | use PHPUnit\Framework\Attributes\PreserveGlobalState; 26 | use PHPUnit\Framework\Attributes\RunInSeparateProcess; 27 | 28 | /** 29 | * @internal 30 | */ 31 | final class SQLTrackRepositoryTest extends CIUnitTestCase 32 | { 33 | use DatabaseTestTrait; 34 | 35 | /** 36 | * @var string 37 | */ 38 | protected $basePath = __DIR__ . '/../src/Database/'; 39 | 40 | /** 41 | * @var string 42 | */ 43 | protected $namespace = 'Album'; 44 | 45 | /** 46 | * @var list 47 | */ 48 | protected $seed = [ 49 | AlbumSeeder::class, 50 | TrackSeeder::class, 51 | ]; 52 | 53 | private $repository; 54 | 55 | protected function setUp(): void 56 | { 57 | parent::setUp(); 58 | 59 | $this->repository = Services::trackRepository(); 60 | } 61 | 62 | public function testfindPaginatedDataWithKeywordNotFoundInDB(): void 63 | { 64 | $album = new Album(); 65 | $album->id = 1; 66 | 67 | $tracks = $this->repository->findPaginatedData($album, 'Pak Ngah'); 68 | $this->assertEmpty($tracks); 69 | } 70 | 71 | public function testfindPaginatedDataWithKeywordFoundInDB(): void 72 | { 73 | $album = new Album(); 74 | $album->id = 1; 75 | 76 | $tracks = $this->repository->findPaginatedData($album, 'Eross Chandra'); 77 | $this->assertNotEmpty($tracks); 78 | } 79 | 80 | public function testFindTrackOfIdWithNotFoundIdInDatabase(): void 81 | { 82 | $this->expectException(TrackNotFoundException::class); 83 | $this->repository->findTrackOfId(random_int(1000, 2000)); 84 | } 85 | 86 | public function testFindTrackOfIdWithFoundIdInDatabase(): void 87 | { 88 | $this->assertInstanceOf(Track::class, $this->repository->findTrackOfId(1)); 89 | } 90 | 91 | /** 92 | * @return array> 93 | */ 94 | public static function invalidData(): array 95 | { 96 | return [ 97 | 'empty array' => [ 98 | [], 99 | ], 100 | 'null' => [ 101 | null, 102 | ], 103 | ]; 104 | } 105 | 106 | /** 107 | * @param list|null $data 108 | */ 109 | #[DataProvider('invalidData')] 110 | public function testSaveInvalidData(?array $data): void 111 | { 112 | $this->assertFalse($this->repository->save($data)); 113 | } 114 | 115 | /** 116 | * @return array{insert: array, update: array} 117 | */ 118 | public static function validData(): array 119 | { 120 | return [ 121 | 'insert' => [ 122 | [ 123 | 'album_id' => 1, 124 | 'title' => 'Sahabat Sejati', 125 | 'author' => 'Erros Chandra', 126 | ], 127 | ], 128 | 'update' => [ 129 | [ 130 | 'id' => 1, 131 | 'album_id' => 1, 132 | 'title' => 'Temani Aku', 133 | 'author' => 'Erros Chandra', 134 | ], 135 | ], 136 | ]; 137 | } 138 | 139 | /** 140 | * @param array|array $data 141 | */ 142 | #[DataProvider('validData')] 143 | #[PreserveGlobalState(false)] 144 | #[RunInSeparateProcess] 145 | public function testSaveValidData(array $data): void 146 | { 147 | $this->assertTrue($this->repository->save($data)); 148 | } 149 | 150 | public function testDeleteTrackOfIdWithNotFoundIdInDatabase(): void 151 | { 152 | $this->expectException(TrackNotFoundException::class); 153 | $this->repository->deleteOfId(random_int(1000, 2000)); 154 | } 155 | 156 | public function testDeleteTrackOfIdWithFoundIdInDatabase(): void 157 | { 158 | $this->assertTrue($this->repository->deleteOfId(1)); 159 | } 160 | 161 | public function testSaveDuplicateDataInsert(): void 162 | { 163 | $this->assertTrue($this->repository->save( 164 | [ 165 | 'album_id' => 1, 166 | 'title' => 'Sahabat Sejati', 167 | 'author' => 'Erros Chandra', 168 | ], 169 | )); 170 | 171 | $this->expectException(DuplicatedRecordException::class); 172 | $this->repository->save( 173 | [ 174 | 'album_id' => 1, 175 | 'title' => 'Sahabat Sejati', 176 | 'author' => 'Erros Chandra', 177 | ], 178 | ); 179 | } 180 | 181 | public function testSaveDuplicateDataUpdate(): void 182 | { 183 | $this->assertTrue($this->repository->save( 184 | [ 185 | 'album_id' => 1, 186 | 'title' => 'Sahabat Sejati', 187 | 'author' => 'Erros Chandra', 188 | ], 189 | )); 190 | 191 | $this->assertTrue($this->repository->save( 192 | [ 193 | 'album_id' => 1, 194 | 'title' => 'Temani Aku', 195 | 'author' => 'Erros Chandra', 196 | ], 197 | )); 198 | 199 | $lastId = Database::connect()->insertID(); 200 | 201 | $this->expectException(DuplicatedRecordException::class); 202 | $this->repository->save( 203 | [ 204 | 'id' => $lastId, 205 | 'album_id' => 1, 206 | 'title' => 'Sahabat Sejati', 207 | 'author' => 'Erros Chandra', 208 | ], 209 | ); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /test/Controller/TrackTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace AlbumTest\Controller; 13 | 14 | use Album\Controllers\Track; 15 | use Album\Database\Seeds\AlbumSeeder; 16 | use Album\Database\Seeds\TrackSeeder; 17 | use CodeIgniter\Test\CIUnitTestCase; 18 | use CodeIgniter\Test\ControllerTestTrait; 19 | use CodeIgniter\Test\DatabaseTestTrait; 20 | use Config\Database; 21 | use Config\Services; 22 | use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; 23 | 24 | #[RunTestsInSeparateProcesses] 25 | /** 26 | * @internal 27 | */ 28 | final class TrackTest extends CIUnitTestCase 29 | { 30 | use DatabaseTestTrait; 31 | use ControllerTestTrait; 32 | 33 | /** 34 | * @var string 35 | */ 36 | protected $basePath = __DIR__ . '/../src/Database/'; 37 | 38 | /** 39 | * @var string 40 | */ 41 | protected $namespace = 'Album'; 42 | 43 | /** 44 | * @var list 45 | */ 46 | protected $seed = [ 47 | AlbumSeeder::class, 48 | TrackSeeder::class, 49 | ]; 50 | 51 | public function testIndexTrackByNotFoundAlbum(): void 52 | { 53 | $testResponse = $this->controller(Track::class) 54 | ->execute('index', 2); 55 | 56 | $this->assertSame(404, $testResponse->response()->getStatusCode()); 57 | } 58 | 59 | public function testIndexTrackHasNoData(): void 60 | { 61 | Database::connect()->disableForeignKeyChecks(); 62 | Database::connect()->table('track')->truncate(); 63 | Database::connect()->enableForeignKeyChecks(); 64 | 65 | $testResponse = $this->controller(Track::class) 66 | ->execute('index', 1); 67 | 68 | $this->assertTrue($testResponse->isOK()); 69 | $this->assertTrue($testResponse->see('No album track found.')); 70 | } 71 | 72 | public function testIndexTrackHasData(): void 73 | { 74 | $testResponse = $this->controller(Track::class) 75 | ->execute('index', 1); 76 | 77 | $this->assertTrue($testResponse->isOK()); 78 | $this->assertTrue($testResponse->see('Eross')); 79 | } 80 | 81 | public function testIndexSearchTrackFound(): void 82 | { 83 | $request = Services::request(); 84 | $request = $request->withMethod('get'); 85 | $request->setGlobal('get', [ 86 | 'keyword' => 'kisah', 87 | ]); 88 | 89 | $testResponse = $this->withRequest($request) 90 | ->controller(Track::class) 91 | ->execute('index', 1); 92 | 93 | $this->assertTrue($testResponse->see('Sebuah Kisah Klasik')); 94 | } 95 | 96 | public function testIndexSearchTrackNotFound(): void 97 | { 98 | $request = Services::request(); 99 | $request = $request->withMethod('get'); 100 | $request->setGlobal('get', [ 101 | 'keyword' => 'Purnama', 102 | ]); 103 | 104 | $testResponse = $this->withRequest($request) 105 | ->controller(Track::class) 106 | ->execute('index', 1); 107 | 108 | $this->assertTrue($testResponse->see('No album track found.')); 109 | } 110 | 111 | public function testAddTrackByNotFoundAlbum(): void 112 | { 113 | $testResponse = $this->controller(Track::class) 114 | ->execute('add', 2); 115 | 116 | $this->assertSame(404, $testResponse->response()->getStatusCode()); 117 | } 118 | 119 | public function testAddTrack(): void 120 | { 121 | $testResponse = $this->controller(Track::class) 122 | ->execute('add', 1); 123 | 124 | $this->assertTrue($testResponse->isOK()); 125 | } 126 | 127 | public function testAddTrackInvalidData(): void 128 | { 129 | $request = Services::request(null, false); 130 | $request = $request->withMethod('post'); 131 | 132 | $testResponse = $this->withRequest($request) 133 | ->controller(Track::class) 134 | ->execute('add', 1); 135 | $this->assertTrue($testResponse->isRedirect()); 136 | $this->seeNumRecords(1, 'track', []); 137 | } 138 | 139 | public function testAddTrackValidData(): void 140 | { 141 | $request = Services::request(); 142 | $request = $request->withMethod('post'); 143 | $request->setGlobal('post', [ 144 | 'album_id' => 1, 145 | 'title' => 'Sahabat Sejati', 146 | 'author' => 'Erros Chandra', 147 | ]); 148 | 149 | $testResponse = $this->withRequest($request) 150 | ->controller(Track::class) 151 | ->execute('add', 1); 152 | 153 | $this->assertTrue($testResponse->isRedirect()); 154 | } 155 | 156 | public function testAddTrackDuplicatedData(): void 157 | { 158 | $request = Services::request(); 159 | $request = $request->withMethod('post'); 160 | $request->setGlobal('post', [ 161 | 'album_id' => 1, 162 | 'title' => 'Sahabat Sejati', 163 | 'author' => 'Erros Chandra', 164 | ]); 165 | 166 | $this->withRequest($request) 167 | ->controller(Track::class) 168 | ->execute('add', 1); 169 | 170 | $this->withRequest($request) 171 | ->controller(Track::class) 172 | ->execute('add', 1); 173 | 174 | $titleError = session()->getFlashdata('errors')['title']; 175 | $this->assertSame('The track with album id 1 has duplicated title.', $titleError); 176 | } 177 | 178 | public function testEditUnexistenceTrack(): void 179 | { 180 | $testResponse = $this->controller(Track::class) 181 | ->execute('edit', random_int(1000, 2000), random_int(1000, 2000)); 182 | 183 | $this->assertSame(404, $testResponse->response()->getStatusCode()); 184 | } 185 | 186 | public function testEditExistenceTrack(): void 187 | { 188 | $testResponse = $this->controller(Track::class) 189 | ->execute('edit', 1, 1); 190 | 191 | $this->assertTrue($testResponse->isOK()); 192 | } 193 | 194 | public function testEditTrackInvalidData(): void 195 | { 196 | $request = Services::request(null, false); 197 | $request = $request->withMethod('post'); 198 | 199 | $testResponse = $this->withRequest($request) 200 | ->controller(Track::class) 201 | ->execute('edit', 1, 1); 202 | $this->assertTrue($testResponse->isRedirect()); 203 | $this->assertNotSame('http://localhost:8080/index.php/album-track/1', $testResponse->getRedirectUrl()); 204 | } 205 | 206 | public function testEditTrackValidData(): void 207 | { 208 | $request = Services::request(); 209 | $request = $request->withMethod('post'); 210 | $request->setGlobal('post', [ 211 | 'id' => 1, 212 | 'album_id' => 1, 213 | 'title' => 'Temani Aku', 214 | 'author' => 'Erros Chandra', 215 | ]); 216 | 217 | $testResponse = $this->withRequest($request) 218 | ->controller(Track::class) 219 | ->execute('edit', 1, 1); 220 | 221 | $this->assertTrue($testResponse->isRedirect()); 222 | $this->assertSame('http://localhost:8080/index.php/album-track/1', $testResponse->getRedirectUrl()); 223 | } 224 | 225 | public function testEditTrackDuplicatedData(): void 226 | { 227 | $request = Services::request(); 228 | $request = $request->withMethod('post'); 229 | $request->setGlobal('post', [ 230 | 'album_id' => 1, 231 | 'title' => 'Sahabat Sejati', 232 | 'author' => 'Erros Chandra', 233 | ]); 234 | 235 | $testResponse = $this->withRequest($request) 236 | ->controller(Track::class) 237 | ->execute('add', 1); 238 | 239 | $this->assertTrue($testResponse->isRedirect()); 240 | 241 | $request->setGlobal('post', [ 242 | 'album_id' => 1, 243 | 'title' => 'Temani Aku', 244 | 'author' => 'Erros Chandra', 245 | ]); 246 | 247 | $this->withRequest($request) 248 | ->controller(Track::class) 249 | ->execute('add', 1); 250 | 251 | $lastId = $this->db->insertID(); 252 | 253 | $request->setGlobal('post', [ 254 | 'id' => $lastId, 255 | 'album_id' => 1, 256 | 'title' => 'Sahabat Sejati', 257 | 'author' => 'Erros Chandra', 258 | ]); 259 | 260 | $this->withRequest($request) 261 | ->controller(Track::class) 262 | ->execute('edit', 1, 1); 263 | 264 | $titleError = session()->getFlashdata('errors')['title']; 265 | $this->assertSame('The track with album id 1 has duplicated title.', $titleError); 266 | } 267 | 268 | public function testDeleteUnexistenceTrack(): void 269 | { 270 | $testResponse = $this->controller(Track::class) 271 | ->execute('delete', random_int(1000, 2000), random_int(1000, 2000)); 272 | 273 | $this->assertSame(404, $testResponse->response()->getStatusCode()); 274 | } 275 | 276 | public function testDeleteExistenceTrack(): void 277 | { 278 | $testResponse = $this->controller(Track::class) 279 | ->execute('delete', 1, 1); 280 | 281 | $this->assertTrue($testResponse->isRedirect()); 282 | } 283 | } 284 | --------------------------------------------------------------------------------