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 |
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 | [](https://github.com/samsonasik/ci4-album/releases)
5 | 
6 | [](https://dashboard.stryker-mutator.io/reports/github.com/samsonasik/ci4-album/master)
7 | [](https://codecov.io/gh/samsonasik/ci4-album)
8 | [](https://github.com/phpstan/phpstan)
9 | [](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 |
--------------------------------------------------------------------------------
|