Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Modularising the Monolith - PHP Conference Japa...

Ryuta Hamasaki
September 24, 2022

Modularising the Monolith - PHP Conference Japan 2022

様々なロジックが密に結合したモノリシックなアプリケーションは開発速度を遅くする一方で、マイクロサービスアーキテクチャはサービス間のコミュニケーションやトランザクションなどといった別の課題があります。
モジュラーモノリスは、モノリシックなアプリケーションの中にドメイン境界を引いて疎結合な状態をつくる、モノリスとマイクロサービスの中間的なアーキテクチャです。

このトークでは、モジュラーモノリスをLaravelアプリケーションに適用する方法を具体例を用いながら紹介します。Laravelを例にしていますが、他のフレームワークにも適用できる内容です。

Ryuta Hamasaki

September 24, 2022
Tweet

More Decks by Ryuta Hamasaki

Other Decks in Technology

Transcript

  1. Verification Solutions Service Provider >1M Accredified documents 9 markets 3

    industries >9M verifications >500 Active issuers A B O U T A C C R E D I F Y
  2. namespace Phpcon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot()

    { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); $this->loadViewsFrom(__DIR__.'/../Resources/views', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', 'order'); $this->commands([DoSomething::class]); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }
  3. namespace Phpcon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot()

    { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); $this->loadViewsFrom(__DIR__.'/../Resources/views', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', 'order'); $this->commands([DoSomething::class]); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }
  4. namespace Phpcon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot()

    { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); $this->loadViewsFrom(__DIR__.'/../Resources/views', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', 'order'); $this->commands([DoSomething::class]); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }
  5. namespace Phpcon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot()

    { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); $this->loadViewsFrom(__DIR__.'/../Resources/views', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', 'order'); $this->commands([DoSomething::class]); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }
  6. namespace Phpcon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot()

    { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); $this->loadViewsFrom(__DIR__.'/../Resources/views', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', 'order'); $this->commands([DoSomething::class]); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }
  7. namespace Phpcon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot()

    { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); $this->loadViewsFrom(__DIR__.'/../Resources/views', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', 'order'); $this->commands([DoSomething::class]); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }
  8. namespace Phpcon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot()

    { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); $this->loadViewsFrom(__DIR__.'/../Resources/views', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', 'order'); $this->commands([DoSomething::class]); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }
  9. namespace Phpcon\Order\Providers; class OrderServiceProvider extends ServiceProvider { public function boot()

    { $this->loadRoutesFrom(__DIR__.'/../routes.php'); $this->loadMigrationsFrom(__DIR__.'/../Infrastructure/Database/Migrations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'order'); $this->loadViewsFrom(__DIR__.'/../Resources/views', 'order'); } public function register() { $this->mergeConfigFrom(__DIR__.'/../config/order.php', 'order'); $this->commands([DoSomething::class]); $this->app->register(AuthServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->register(RouteServiceProvider::class); } }
  10. namespace Phpcon\Order\Providers; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Laracon\Order\Application\Policies\OrderPolicy; use Laracon\Order\Domain\Models\Order;

    class AuthServiceProvider extends ServiceProvider { protected $policies = [ Order::class => OrderPolicy::class, ]; public function boot() { $this->registerPolicies(); } }
  11. namespace Phpcon\Order\Providers; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Laracon\Order\Domain\Listeners\HandleOrderShipment; use Laracon\Shipping\Contracts\Events\ParcelShipped;

    class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ ParcelShipped::class => [ HandleOrderShipment::class, ], ]; }
  12. namespace Phpcon\Order\Providers; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Route; use Phpcon\Order\Application\Http\Middleware\SomeMiddleware;

    use Phpcon\Order\Domain\Models\Order; class RouteServiceProvider extends ServiceProvider { public function boot() { Route::bind('order', function ($value) { return Order::with('orderLines')->findOrFail($value); }); $this->app[‘router']->aliasMiddleware('do-something', SomeMiddleware::class); } }
  13. { "autoload": { "psr-4": { "App\\": "app/", "Phpcon\\": "src/", "Database\\Factories\\":

    "database/factories/", "Database\\Seeders\\": "database/seeders/" } } } // composer.json
  14. { "autoload": { "psr-4": { "App\\": "app/", "Phpcon\\": "src/", "Database\\Factories\\":

    "database/factories/", "Database\\Seeders\\": "database/seeders/" } } } // composer.json
  15. Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart

    Item Order Order Line 2. Update inventory 1. Checkout
  16. Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart

    Item Order Order Line 2. Update inventory 3. Process payment 1. Checkout
  17. Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart

    Item Order Order Line 2. Update inventory 3. Process payment 4. Save order 1. Checkout
  18. Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart

    Item Order Order Line 2. Update inventory 3. Process payment 5. Notify warehouse 4. Save order 1. Checkout
  19. class OrderController extends Controller { public function store(StoreOrderRequest $request) {

    $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  20. class OrderController extends Controller { public function store(StoreOrderRequest $request) {

    $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  21. class OrderController extends Controller { public function store(StoreOrderRequest $request) {

    $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  22. class OrderController extends Controller { public function store(StoreOrderRequest $request) {

    $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  23. class OrderController extends Controller { public function store(StoreOrderRequest $request) {

    $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  24. class OrderController extends Controller { public function store(StoreOrderRequest $request) {

    $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  25. class OrderController extends Controller { public function store(StoreOrderRequest $request) {

    $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  26. class OrderController extends Controller { public function store(StoreOrderRequest $request) {

    $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  27. namespace Laracon\Order\Domain\Models; class CartItem extends Model { // ... public

    function cart(): BelongsTo { return $this->belongsTo(Cart::class); } public function product(): BelongsTo { return $this->belongsTo(Product::class); } }
  28. namespace Laracon\Order\Domain\Models; class CartItem extends Model { // ... public

    function cart(): BelongsTo { return $this->belongsTo(Cart::class); } public function product(): BelongsTo { return $this->belongsTo(Product::class); } }
  29. // src/Inventory/Contracts/ProductService.php namespace Laracon\Inventory\Contracts; interface ProductService { /** * Decrement

    product stock. */ public function decrementStock(int $productId, int $quantity): void; }
  30. namespace Laracon\Inventory\Infrastructure\Services; use Laracon\Inventory\Contracts\ProductService as ProductServiceContract; class ProductService implements ProductServiceContract

    { public function decrementStock(int $productId, int $quantity): void { $product = Product::find($productId); if (!$product) { throw new ProductNotFoundException($productId); } if ($product->stock < $quantity) { throw new OutOfStockException($productId); } if (!$product->is_active) { throw new InactiveProductException($productId); } $product->decrement('stock', $quantity); } }
  31. namespace Laracon\Inventory\Infrastructure\Services; use Laracon\Inventory\Contracts\ProductService as ProductServiceContract; class ProductService implements ProductServiceContract

    { public function decrementStock(int $productId, int $quantity): void { $product = Product::find($productId); if (!$product) { throw new ProductNotFoundException($productId); } if ($product->stock < $quantity) { throw new OutOfStockException($productId); } if (!$product->is_active) { throw new InactiveProductException($productId); } $product->decrement('stock', $quantity); } }
  32. use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private

    ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  33. use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private

    ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $cartItem->product->decrement('stock', $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  34. use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private

    ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $this->productService->decrementStock($cartItem->product_id, $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  35. use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private

    ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $this->productService->decrementStock($cartItem->product_id, $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  36. namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Domain\Models\Product; class Order extends Model { //

    ... protected $orderLines = []; public function orderLines(): HasMany { return $this->hasMany(OrderLine::class); } public function addOrderLine(Product $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }
  37. namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Domain\Models\Product; class Order extends Model { //

    ... protected $orderLines = []; public function orderLines(): HasMany { return $this->hasMany(OrderLine::class); } public function addOrderLine(Product $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }
  38. namespace Laracon\Inventory\Infrastructure\Services; use Laracon\Inventory\Contracts\DataTransferObjects\ProductDto; use Laracon\Inventory\Contracts\ProductService as ProductServiceContract; use Laracon\Inventory\Domain\Models\Product;

    class ProductService implements ProductServiceContract { public function getProductById(int $productId): ProductDto { $product = Product::find($productId); if (!$product) { throw new ProductNotFoundException($productId); } return new ProductDto( $product->id, $product->name, $product->price, ); } }
  39. use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private

    ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $this->productService->decrementStock($cartItem->product_id, $cartItem->quantity); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  40. use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private

    ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $this->productService->decrementStock($cartItem->product_id, $cartItem->quantity); $product = $this->productService->getProductById($cartItem->product_id); $order->addOrderLine($cartItem->product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  41. use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private

    ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { $cart->cartItems->each(function (CartItem $cartItem) use ($order) { $this->productService->decrementStock($cartItem->product_id, $cartItem->quantity); $product = $this->productService->getProductById($cartItem->product_id); $order->addOrderLine($product, $cartItem->quantity); }); // ... }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  42. namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Domain\Models\Product; class Order extends Model { //

    ... protected $orderLines = []; public function addOrderLine(Product $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }
  43. namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Domain\Models\Product; class Order extends Model { //

    ... protected $orderLines = []; public function addOrderLine(Product $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }
  44. namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Contracts\DataTransferObjects\ProductDto; class Order extends Model { //

    ... protected $orderLines = []; public function addOrderLine(ProductDto $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }
  45. namespace Laracon\Order\Domain\Models; use Laracon\Inventory\Contracts\DataTransferObjects\ProductDto; class Order extends Model { //

    ... protected $orderLines = []; public function addOrderLine(ProductDto $product, int $quantity): void { $orderLine = new OrderLine([ 'product_id' => $product->id, 'product_name' => $product->name, 'price' => $product->price, 'quantity' => $quantity, ]); $this->orderLines[] = $orderLine; } }
  46. use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private

    ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { // ... $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  47. use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(private

    ProductService $productService) {} public function store(StoreOrderRequest $request) { $cart = Cart::with('cartItems')->findOrFail($request->cart_id); $order = new Order(['user_id' => $request->user()->id]); try { DB::transaction(function () use ($order, $cart) { // ... $order->checkout(); $stripe = new StripePayment(config(‘services.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  48. namespace Laracon\Payment\Contracts; interface PaymentService { /** * Process payment for

    the given amount. */ public function charge(int $orderId, int $amount): void; }
  49. namespace Laracon\Payment\Infrastructure\Services; use Stripe\StripeClient; use Laracon\Payment\Contracts\PaymentService as PaymentServiceContract; class StripePayment

    implements PaymentServiceContract { private StripeClient $stripe; public function __construct(string $stripeKey) { $this->stripe = new StripeClient($stripeKey); } public function charge(int $orderId, int $amount): void { $this->stripe->charges->create([ 'amount' => $amount, 'currency' => 'usd', ]); // ... } }
  50. namespace Laracon\Payment\Providers; use Illuminate\Support\ServiceProvider; use Laracon\Payment\Contracts\PaymentService as PaymentServiceContract; use Laracon\Payment\Infrastructure\Services\StripePayment;

    class PaymentServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PaymentServiceContract::class, function ($app) { return new StripePayment(config(‘services.stripe.key')); }); } }
  51. use Laracon\Inventory\Contracts\ProductService; class OrderController extends Controller { public function __construct(

    private ProductService $productService, ) {} public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $stripe = new StripePayment(config('service.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  52. use Laracon\Inventory\Contracts\ProductService; use Laracon\Payment\Contracts\PaymentService; class OrderController extends Controller { public

    function __construct( private ProductService $productService, private PaymentService $paymentService, ) {} public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $stripe = new StripePayment(config('service.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  53. use Laracon\Inventory\Contracts\ProductService; use Laracon\Payment\Contracts\PaymentService; class OrderController extends Controller { public

    function __construct( private ProductService $productService, private PaymentService $paymentService, ) {} public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $stripe = new StripePayment(config('service.stripe.key')); $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  54. use Laracon\Inventory\Contracts\ProductService; use Laracon\Payment\Contracts\PaymentService; class OrderController extends Controller { public

    function __construct( private ProductService $productService, private PaymentService $paymentService, ) {} public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $stripe->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  55. use Laracon\Inventory\Contracts\ProductService; use Laracon\Payment\Contracts\PaymentService; class OrderController extends Controller { public

    function __construct( private ProductService $productService, private PaymentService $paymentService, ) {} public function store(StoreOrderRequest $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $this->paymentService->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  56. namespace Laracon\Payment\Providers; use Illuminate\Support\ServiceProvider; use Laracon\Payment\Contracts\PaymentService as PaymentServiceContract; use Laracon\Payment\Infrastructure\Services\StripePayment;

    class PaymentServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PaymentServiceContract::class, function ($app) { return new StripePayment(config(‘services.stripe.key')); }); } }
  57. namespace Laracon\Payment\Providers; use Illuminate\Support\ServiceProvider; use Laracon\Payment\Contracts\PaymentService as PaymentServiceContract; use Laracon\Payment\Infrastructure\Services\PaddlePayment;

    class PaymentServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PaymentServiceContract::class, function ($app) { return new PaddlePayment(config(‘services.paddle.key’)); }); } }
  58. Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart

    Item Order Order Line 2. Update inventory 3. Process payment 5. Notify warehouse 4. Save order 1. Checkout
  59. Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart

    Item Order Order Line OrderFul fi lled Publish Subscribe 2. Update inventory 3. Process payment 4. Save order 1. Checkout
  60. class OrderController extends Controller { // ... public function store(StoreOrderRequest

    $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $this->paymentService->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } return new OrderResource($order); } }
  61. class OrderController extends Controller { // ... public function store(StoreOrderRequest

    $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $this->paymentService->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } OrderFulfilled::dispatch($order->id); return new OrderResource($order); } }
  62. class OrderController extends Controller { // ... public function store(StoreOrderRequest

    $request) { // ... try { DB::transaction(function () use ($order, $cart) { // ... $this->paymentService->charge($order->id, $order->total_amount); }); } catch (\Exception $e) { abort(Response::HTTP_BAD_REQUEST, trans('order::errors.failed')); } OrderFulfilled::dispatch($order->id); return new OrderResource($order); } }
  63. <phpunit> <testsuites> <testsuite name="Unit"> <directory suffix="Test.php">./src/**/Tests/Unit</directory> </testsuite> <testsuite name="Feature"> <directory

    suffix="Test.php">./src/**/Tests/Feature</directory> </testsuite> </testsuites> <coverage processUncoveredFiles="true"> <include> <directory suffix=".php">./src</directory> </include> <exclude> <directory suffix="Test.php">./src/**/Tests/*</directory> </exclude> <report> <clover outputFile="coverage/clover.xml"/> </report> </coverage> <!-- ... --> </phpunit>
  64. <phpunit> <testsuites> <testsuite name="Unit"> <directory suffix="Test.php">./src/**/Tests/Unit</directory> </testsuite> <testsuite name="Feature"> <directory

    suffix="Test.php">./src/**/Tests/Feature</directory> </testsuite> </testsuites> <coverage processUncoveredFiles="true"> <include> <directory suffix=".php">./src</directory> </include> <exclude> <directory suffix="Test.php">./src/**/Tests/*</directory> </exclude> <report> <clover outputFile="coverage/clover.xml"/> </report> </coverage> <!-- ... --> </phpunit>
  65. <phpunit> <testsuites> <testsuite name="Unit"> <directory suffix="Test.php">./src/**/Tests/Unit</directory> </testsuite> <testsuite name="Feature"> <directory

    suffix="Test.php">./src/**/Tests/Feature</directory> </testsuite> </testsuites> <coverage processUncoveredFiles="true"> <include> <directory suffix=".php">./src</directory> </include> <exclude> <directory suffix="Test.php">./src/**/Tests/*</directory> </exclude> <report> <clover outputFile="coverage/clover.xml"/> </report> </coverage> <!-- ... --> </phpunit>
  66. it('creates a new order', function () { Event::fake(); $user =

    User::factory()->create(); $cart = Cart::factory()->create(['user_id' => $user->id]); // create some other test data... mock(ProductService::class, function ($mock) use ($product) { $mock->shouldReceive('decrementStock') ->with($product->id, 1) ->once(); $mock->shouldReceive('getProductById') ->with($product->id) ->once() ->andReturn($product); }); mock(PaymentService::class) ->shouldReceive('charge') ->once(); // ...
  67. it('creates a new order', function () { Event::fake(); $user =

    User::factory()->create(); $cart = Cart::factory()->create(['user_id' => $user->id]); // create some other test data... mock(ProductService::class, function ($mock) use ($product) { $mock->shouldReceive('decrementStock') ->with($product->id, 1) ->once(); $mock->shouldReceive('getProductById') ->with($product->id) ->once() ->andReturn($product); }); mock(PaymentService::class) ->shouldReceive('charge') ->once(); // ...
  68. it('creates a new order', function () { Event::fake(); $user =

    User::factory()->create(); $cart = Cart::factory()->create(['user_id' => $user->id]); // create some other test data... mock(ProductService::class, function ($mock) use ($product) { $mock->shouldReceive('decrementStock') ->with($product->id, 1) ->once(); $mock->shouldReceive('getProductById') ->with($product->id) ->once() ->andReturn($product); }); mock(PaymentService::class) ->shouldReceive('charge') ->once(); // ...
  69. it('creates a new order', function () { Event::fake(); $user =

    User::factory()->create(); $cart = Cart::factory()->create(['user_id' => $user->id]); // create some other test data... mock(ProductService::class, function ($mock) use ($product) { $mock->shouldReceive('decrementStock') ->with($product->id, 1) ->once(); $mock->shouldReceive('getProductById') ->with($product->id) ->once() ->andReturn($product); }); mock(PaymentService::class) ->shouldReceive('charge') ->once(); // ...
  70. // ... $order = postJson('/order-module/orders', ['cart_id' => $cart->id]) ->assertCreated() ->json('data');

    assertDatabaseHas('orders', [ 'id' => $order['id'], ]); Event::assertDispatched(OrderFulfilled::class, $order['id']);
  71. parameters: paths: - ./src - ./app layers: - name: Common

    collectors: - type: directory regex: ./app/.* - name: Contracts collectors: - type: directory regex: .*/Contracts/.* - name: Vendor collectors: - type: bool must_not: - type: directory regex: ./(src|app)/.* - name: Inventory collectors: - type: bool must: - type: directory regex: ./src/Inventory/.* must_not: - type: directory regex: ./src/Inventory/Contracts/.* // deptrac.yaml
  72. parameters: paths: - ./src - ./app layers: - name: Common

    collectors: - type: directory regex: ./app/.* - name: Contracts collectors: - type: directory regex: .*/Contracts/.* - name: Vendor collectors: - type: bool must_not: - type: directory regex: ./(src|app)/.* - name: Inventory collectors: - type: bool must: - type: directory regex: ./src/Inventory/.* must_not: - type: directory regex: ./src/Inventory/Contracts/.* // deptrac.yaml
  73. parameters: paths: - ./src - ./app layers: - name: Common

    collectors: - type: directory regex: ./app/.* - name: Contracts collectors: - type: directory regex: .*/Contracts/.* - name: Vendor collectors: - type: bool must_not: - type: directory regex: ./(src|app)/.* - name: Inventory collectors: - type: bool must: - type: directory regex: ./src/Inventory/.* must_not: - type: directory regex: ./src/Inventory/Contracts/.* // deptrac.yaml
  74. parameters: paths: - ./src - ./app layers: - name: Common

    collectors: - type: directory regex: ./app/.* - name: Contracts collectors: - type: directory regex: .*/Contracts/.* - name: Vendor collectors: - type: bool must_not: - type: directory regex: ./(src|app)/.* - name: Inventory collectors: - type: bool must: - type: directory regex: ./src/Inventory/.* must_not: - type: directory regex: ./src/Inventory/Contracts/.* // deptrac.yaml
  75. parameters: paths: - ./src - ./app layers: - name: Common

    collectors: - type: directory regex: ./app/.* - name: Contracts collectors: - type: directory regex: .*/Contracts/.* - name: Vendor collectors: - type: bool must_not: - type: directory regex: ./(src|app)/.* - name: Inventory collectors: - type: bool must: - type: directory regex: ./src/Inventory/.* must_not: - type: directory regex: ./src/Inventory/Contracts/.* // deptrac.yaml
  76. parameters: paths: - ./src - ./app layers: - name: Common

    collectors: - type: directory regex: ./app/.* - name: Contracts collectors: - type: directory regex: .*/Contracts/.* - name: Vendor collectors: - type: bool must_not: - type: directory regex: ./(src|app)/.* - name: Inventory collectors: - type: bool must: - type: directory regex: ./src/Inventory/.* must_not: - type: directory regex: ./src/Inventory/Contracts/.* // deptrac.yaml
  77. parameters: paths: - ./src - ./app layers: - name: Common

    collectors: - type: directory regex: ./app/.* - name: Contracts collectors: - type: directory regex: .*/Contracts/.* - name: Vendor collectors: - type: bool must_not: - type: directory regex: ./(src|app)/.* - name: Inventory collectors: - type: bool must: - type: directory regex: ./src/Inventory/.* must_not: - type: directory regex: ./src/Inventory/Contracts/.* // deptrac.yaml
  78. parameters: paths: - ./src - ./app layers: - name: Common

    collectors: - type: directory regex: ./app/.* - name: Contracts collectors: - type: directory regex: .*/Contracts/.* - name: Vendor collectors: - type: bool must_not: - type: directory regex: ./(src|app)/.* - name: Inventory collectors: - type: bool must: - type: directory regex: ./src/Inventory/.* must_not: - type: directory regex: ./src/Inventory/Contracts/.* // deptrac.yaml
  79. parameters: paths: - ./src - ./app layers: # ... ruleset:

    Common: - Vendor Contracts: - Vendor - Common Inventory: - Contracts - Vendor - Common Order: - Contracts - Vendor - Common Payment: - Contracts - Vendor - Common Shipping: - Contracts - Vendor - Common // deptrac.yaml
  80. parameters: paths: - ./src - ./app layers: # ... ruleset:

    Common: - Vendor Contracts: - Vendor - Common Inventory: - Contracts - Vendor - Common Order: - Contracts - Vendor - Common Payment: - Contracts - Vendor - Common Shipping: - Contracts - Vendor - Common // deptrac.yaml
  81. parameters: paths: - ./src - ./app layers: # ... ruleset:

    Common: - Vendor Contracts: - Vendor - Common Inventory: - Contracts - Vendor - Common Order: - Contracts - Vendor - Common Payment: - Contracts - Vendor - Common Shipping: - Contracts - Vendor - Common // deptrac.yaml
  82. parameters: paths: - ./src - ./app layers: # ... ruleset:

    Common: - Vendor Contracts: - Vendor - Common Inventory: - Contracts - Vendor - Common Order: - Contracts - Vendor - Common Payment: - Contracts - Vendor - Common Shipping: - Contracts - Vendor - Common // deptrac.yaml
  83. parameters: paths: - ./src - ./app layers: # ... ruleset:

    Common: - Vendor Contracts: - Vendor - Common Inventory: - Contracts - Vendor - Common Order: - Contracts - Vendor - Common Payment: - Contracts - Vendor - Common Shipping: - Contracts - Vendor - Common // deptrac.yaml
  84. ϦϯΫू •https://github.com/avosalmon/modular-monolith-laravel •Modularising the Monolith - Laracon Online 2022 •Deconstructing

    the Monolith: Designing Software that Maximizes Developer Productivity •Under Deconstruction: The State of Shopify’s Monolith •Long live the Monolith! Monolithic Architecture != Big Ball of Mud •Strategic Monoliths and Microservices •Domain-Driven Laravel