-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
478 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"languageToolLinter.enabled": false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
--- | ||
title: Arsitektur | ||
--- | ||
|
||
Aplikasi didesain menggunakan DDD (_domain-driven design_) dan _onion | ||
architecture_. _Onion architecture_ adalah arsitektur yang memisahkan antara | ||
business logic dengan teknologi yang digunakan. Dengan _onion architecture_, | ||
aplikasi akan lebih mudah diuji, dikembangkan, dan dipelihara. | ||
|
||
Aplikasi kita dibagi menjadi beberapa layer, dimulai dari layer terdalam: | ||
|
||
* Layer **domain**. Namespace `App\Domain`. Terdiri dari entity, value object | ||
dan objek-objek pendukung, seperti collection, domain event, exception, dan | ||
lain-lain. | ||
|
||
* Layer **domain service**. Namespace `App\DomainService`. Terdiri dari | ||
repository, event listener, dan service lain yang hanya memiliki dependency ke | ||
layer domain. | ||
|
||
* Layer **application service**. Namespace `App\ApplicationService`. Terdiri | ||
dari application-level event listener, messenger handler, dan service | ||
semacamnya. | ||
|
||
* Layer-layer luar, terdiri dari: | ||
|
||
* Layer **infrastructure**. Namespace `App\Infrastructure`. Terdiri dari | ||
implementasi dari repository, event listener, dan service lain yang | ||
berinteraksi dengan teknologi eksternal. | ||
|
||
* Layer **front end**. Berkaitan dengan interaksi aplikasi dengan pengguna. | ||
Terdiri dari: | ||
|
||
* Layer **web front end**. Namespace `App\WebFrontEnd`. Terdiri dari | ||
web controller, form, template, dan service lain yang berhubungan dengan | ||
web. | ||
|
||
* Layer **API front end**. Namespace `App\ApiFrontEnd`. Terdiri dari | ||
API resource, API state, konfigurasi mapper, dan sebagainya. | ||
|
||
* Layer **console front end**. Namespace `App\Command`. Terdiri dari | ||
console command, dan service lain yang berhubungan. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
title: Arsitektur | ||
--- | ||
|
||
import DocCardList from '@theme/DocCardList'; | ||
|
||
<DocCardList /> |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
--- | ||
title: Domain | ||
--- | ||
|
||
Untuk memulai, pertama-tama kita perlu desain domain-nya. Komponen utama domain | ||
adalah entity dan value object. Bedanya, entity memiliki state, dan biasanya | ||
akan disimpan di database. Sedangkan value object tidak memiliki state. | ||
|
||
:::caution | ||
|
||
Pada tahap ini kita tidak perlu memikirkan teknologi yang digunakan, seperti | ||
database dan sebagainya. Kita hanya perlu fokus pada model bisnisnya saja. | ||
|
||
::: | ||
|
||
## Entity `Post` | ||
|
||
Berikut contoh entity untuk `Post`: | ||
|
||
```php | ||
namespace App\Domain; | ||
|
||
use Rekalogika\CommonBundle\Domain\Entity\AbstractAggregateRoot; | ||
use Symfony\Component\Clock\DatePoint; | ||
|
||
class Post extends AbstractAggregateRoot | ||
{ | ||
private \DateTimeInterface $createdTime; | ||
private string $title; | ||
private string $content; | ||
|
||
public function __construct( | ||
string $title, | ||
string $content, | ||
) { | ||
$this->title = $title; | ||
$this->createdTime = new DatePoint(); | ||
} | ||
} | ||
``` | ||
|
||
Terlihat bahwa class ini sudah bisa mewakili fungsi dasar dari blog post. Setiap | ||
blog post memiliki judul, konten, dan waktu pembuatan. Waktu pembuatan otomatis | ||
dibuat saat objek dibuat. Sedangkan terlihat `title` dan `content` adalah | ||
property yang wajib ada (tidak boleh kosong), dan harus ada saat objek pertama | ||
kali dibuat. | ||
|
||
:::info | ||
|
||
`DateTimeInterface` adalah interface dari semua objek waktu di PHP. | ||
Implementasi di PHP berupa `DateTimeImmutable` dan `DateTime`. Di sini kita | ||
menggunakan `DatePoint` dari Symfony Clock. Alasannya karena `DatePoint` lebih | ||
mudah diuji daripada `DateTimeImmutable` dan `DateTime`. | ||
|
||
::: | ||
|
||
Properti `createdTime` tidak boleh diubah setelah, hanya bisa dilihat, maka kita | ||
jadikan `readonly` dan hanya kita buatkan getternya, tidak perlu setter: | ||
|
||
```php | ||
class Post extends AbstractAggregateRoot | ||
{ | ||
private readonly \DateTimeInterface $createdTime; | ||
|
||
public function getCreatedTime(): \DateTimeInterface | ||
{ | ||
return $this->createdTime; | ||
} | ||
} | ||
``` | ||
|
||
:::info | ||
|
||
Objek `DateTimeInterface` adalah salah satu contoh dari value object. | ||
|
||
::: | ||
|
||
Karena `title` dan `content` boleh diubah setelah pos dibuat, maka kita buatkan | ||
getter dan setternya: | ||
|
||
```php | ||
class Post extends AbstractAggregateRoot | ||
{ | ||
public function getTitle(): string | ||
{ | ||
return $this->title; | ||
} | ||
|
||
public function setTitle(string $title): void | ||
{ | ||
$this->title = $title; | ||
} | ||
|
||
public function getContent(): string | ||
{ | ||
return $this->content; | ||
} | ||
|
||
public function setContent(string $content): void | ||
{ | ||
$this->content = $content; | ||
} | ||
} | ||
``` | ||
|
||
## Entity `Comment` | ||
|
||
Karena setiap pos bisa memiliki komentar, maka kita buat entity komentar: | ||
|
||
```php | ||
namespace App\Domain; | ||
|
||
use Rekalogika\CommonBundle\Domain\Entity\AbstractEntity; | ||
use Symfony\Component\Clock\DatePoint; | ||
|
||
class Comment extends AbstractEntity | ||
{ | ||
private string $content; | ||
private readonly \DateTimeInterface $createdTime; | ||
|
||
public function __construct(string $content) | ||
{ | ||
$this->content = $content; | ||
$this->createdTime = new DatePoint(); | ||
} | ||
|
||
public function getCreatedTime(): \DateTimeInterface | ||
{ | ||
return $this->createdTime; | ||
} | ||
} | ||
``` | ||
|
||
Kurang lebih sama perilakunya dengan `Post`. Komentar memiliki konten dan waktu. | ||
|
||
## Relasi Pada Sisi `Post` | ||
|
||
Karena setiap pos bisa memiliki beberapa komentar, maka kita perlu membuat | ||
relasinya. Kita bisa membuat property `comments` di `Post`: | ||
|
||
```php | ||
use Doctrine\Common\Collections\Collection; | ||
use Rekalogika\Contracts\Collections\Recollection; | ||
use Rekalogika\Domain\Collections\ArrayCollection; | ||
use Rekalogika\Domain\Collections\RecollectionDecorator; | ||
|
||
class Post extends AbstractAggregateRoot | ||
{ | ||
/** | ||
* @var Collection<string,Comment> | ||
*/ | ||
private Collection $comments; | ||
|
||
public function __construct( | ||
// ... | ||
) { | ||
// ... | ||
$this->comments = new ArrayCollection(); | ||
} | ||
|
||
public function addComment(Comment $comment): void | ||
{ | ||
$this->getComments()->add($comment); | ||
$comment->setPost($this); | ||
} | ||
|
||
/** | ||
* @return Recollection<string,Comment> | ||
*/ | ||
public function getComments(): Recollection | ||
{ | ||
return RecollectionDecorator::create( | ||
collection: $this->insurables, | ||
count: new PrecountingStrategy($this->insurablesCount), | ||
indexBy: 'id', | ||
orderBy: ['id' => Order::Descending] | ||
); | ||
} | ||
|
||
public function removeComment(Comment $comment): void | ||
{ | ||
if ($this->getComments()->removeElement($comment)) { | ||
if ($comment->getPost() === $this) { | ||
$comment->setPost(null); | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Yang perlu diperhatikan: | ||
|
||
* Untuk menampung komentar, harus menggunakan tipe `Collection`, tidak boleh | ||
array biasa. | ||
* Di atasnya kita tambahkan `@var Collection<string,Comment>` untuk memberi tahu | ||
bahwa `comments` adalah collection dari beberapa objek `Comment` dengan key | ||
bertipe `string`. | ||
|
||
:::info Perbedaan Dengan Symfony dan Doctrine Standard | ||
|
||
Kita menggunakan `ArrayCollection` versi kita, bukan yang dari Doctrine, | ||
[penjelasannya di | ||
sini](https://rekalogika.dev/collections/implementations/array-collection). | ||
|
||
Untuk `getComments()`, kita menggunakan `RecollectionDecorator`, bukan plain | ||
`Collection`. Alasannya: | ||
|
||
* Mengimplementasikan `PageableInterface` yang bisa digunakan langsung untuk | ||
batch processing dan pagination | ||
* Otomatis melakukan limit, sehingga tidak akan menghabiskan memori. | ||
|
||
Cek infonya [di sini](https://rekalogika.dev/collections). | ||
|
||
Yang mengakses `$this->comments` hanyalah method `getComments()`. Method lain | ||
harus menggunakan method `getComments()` untuk mengakses data komentar. Ini kita | ||
lakukan untuk menghindari kesalahan pemrograman yang dapat mengakibatkan | ||
out-of-memory. | ||
|
||
::: | ||
|
||
## Relasi Pada Sisi `Comment` | ||
|
||
Lalu untuk `Comment`, kita bisa menambahkan property `post`: | ||
|
||
```php | ||
class Comment extends AbstractEntity | ||
{ | ||
private ?Post $post = null; | ||
|
||
public function setPost(?Post $post): void | ||
{ | ||
$this->post = $post; | ||
} | ||
|
||
public function getPost(): Post | ||
{ | ||
return $this->post; | ||
} | ||
} | ||
``` | ||
|
||
Jika `$post` null, maka komentar tersebut tidak terhubung dengan pos manapun. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
--- | ||
title: Tutorial | ||
--- | ||
|
||
import DocCardList from '@theme/DocCardList'; | ||
|
||
Untuk tutorial ini, kita akan membuat aplikasi blog sederhana. Aplikasi ini | ||
terdiri dari dua entitas utama: `Post` dan `Comment`. | ||
|
||
<DocCardList /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.