Share
Embed
Modularising the Monolith - PHP Conference Japan 2022
様々なロジックが密に結合したモノリシックなアプリケーションは開発速度を遅くする一方で、マイクロサービスアーキテクチャはサービス間のコミュニケーションやトランザクションなどといった別の課題があります。
モジュラーモノリスは、モノリシックなアプリケーションの中にドメイン境界を引いて疎結合な状態をつくる、モノリスとマイクロサービスの中間的なアーキテクチャです。
このトークでは、モジュラーモノリスをLaravelアプリケーションに適用する方法を具体例を用いながら紹介します。Laravelを例にしていますが、他のフレームワークにも適用できる内容です。
Ryuta Hamasaki
September 24, 2022
Transcript
146 slides
-
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 -
"HFOEB w ϞϊϦεͱϚΠΫϩαʔϏεͷϝϦοτɾσϝϦοτ w ϞδϡϥʔϞϊϦεͱ w -BSBWFMΞϓϦέʔγϣϯͷϞδϡʔϧԽ w Ϟδϡʔϧؒͷίϛϡχέʔγϣϯ w
ϞδϡϥʔϞϊϦεͳΞϓϦέʔγϣϯͷςετ w ੩తίʔυղੳͰυϝΠϯڥքΛڧ੍ -
.PEVMBS.POPMJUI Contracts Implementation Tests Contracts Implementation Tests Contracts Implementation Tests
Inventory module Order module Payment module -
Contracts Implementation Tests Contracts Implementation Tests Contracts Implementation Tests Inventory
module Order module Payment module .PEVMBS.POPMJUI -
Contracts Implementation Tests Contracts Implementation Tests Contracts Implementation Tests Inventory
service Order module Payment module ஈ֊తͳϚΠΫϩαʔϏεԽ -
ϞδϡϥʔϞϊϦεมߋίετ͕͍ Contracts Implementation Tests Contracts Implementation Tests Contracts Implementation Tests
Inventory module Order module Payment module -
ϞδϡʔϧͱνʔϜߏ Order Module Payment Module Shipping Module Campaign Module Auth
Module Inventory Module -
<?php use Illuminate\Support\Facades\Route; Route::prefix('order-module') ->middleware(['api', 'auth:sanctum']) ->group(function () { Route::apiResource('orders',
OrderController::class); }); // src/Order/routes.php -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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(); } } -
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, ], ]; } -
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); } } -
{ "autoload": { "psr-4": { "App\\": "app/", "Phpcon\\": "src/", "Database\\Factories\\":
"database/factories/", "Database\\Seeders\\": "database/seeders/" } } } // composer.json -
{ "autoload": { "psr-4": { "App\\": "app/", "Phpcon\\": "src/", "Database\\Factories\\":
"database/factories/", "Database\\Seeders\\": "database/seeders/" } } } // composer.json -
Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart
Item Order Order Line -
Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart
Item Order Order Line 1. Checkout -
Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart
Item Order Order Line 2. Update inventory 1. Checkout -
Order Inventory Payment Shipping Payment Provider Warehouse System Cart Cart
Item Order Order Line 2. Update inventory 3. Process payment 1. Checkout -
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 -
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 -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
// src/Inventory/Contracts/ProductService.php namespace Laracon\Inventory\Contracts; interface ProductService { /** * Decrement
product stock. */ public function decrementStock(int $productId, int $quantity): void; } -
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); } } -
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); } } -
namespace Laracon\Inventory\Providers; use Laracon\Inventory\Contracts\ProductService as ProductServiceContract; use Laracon\Inventory\Infrastructure\Services\ProductService; class InventoryServiceProvider
extends ServiceProvider { public $bindings = [ ProductServiceContract::class => ProductService::class, ]; // ... } -
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); } } -
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); } } -
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); } } -
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); } } -
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; } } -
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; } } -
namespace Laracon\Inventory\Contracts\DataTransferObjects; class ProductDto { public function __construct( public readonly
int $id, public readonly string $name, public readonly int $price, ) {} } -
namespace Laracon\Inventory\Contracts; use Laracon\Inventory\Contracts\DataTransferObjects\ProductDto; interface ProductService { /** * Get
product by product id. */ public function getProductById(int $productId): ProductDto; } -
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, ); } } -
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); } } -
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); } } -
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); } } -
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; } } -
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; } } -
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; } } -
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; } } -
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); } } -
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); } } -
namespace Laracon\Payment\Contracts; interface PaymentService { /** * Process payment for
the given amount. */ public function charge(int $orderId, int $amount): void; } -
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', ]); // ... } } -
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')); }); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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); } } -
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')); }); } } -
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’)); }); } } -
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 -
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 -
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); } } -
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); } } -
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); } } -
namespace Laracon\Shipping\Providers; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Laracon\Order\Contracts\Events\OrderFulfilled; use Laracon\Shipping\Domain\Listeners\NotifyWarehouse;
class EventServiceProvider extends ServiceProvider { protected $listen = [ OrderFulfilled::class => [ NotifyWarehouse::class, ] ]; } -
namespace Laracon\Shipping\Domain\Listeners; use Laracon\Order\Contracts\Events\OrderFulfilled; class NotifyWarehouse { public function handle(OrderFulfilled
$event) { // Notify warehouse system } } -
<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> -
<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> -
<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> -
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(); // ... -
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(); // ... -
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(); // ... -
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(); // ... -
// ... $order = postJson('/order-module/orders', ['cart_id' => $cart->id]) ->assertCreated() ->json('data');
assertDatabaseHas('orders', [ 'id' => $order['id'], ]); Event::assertDispatched(OrderFulfilled::class, $order['id']); -
composer require qossmic/deptrac-shim --dev sudo apt-get install graphviz // for
visualising dependency graph -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
ϦϯΫू •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