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

PHPで学ぶプログラミングの教訓 / Lessons in Programming Learn...

nrs
December 22, 2024

PHPで学ぶプログラミングの教訓 / Lessons in Programming Learned through PHP

PHP Conference Japan 2024 のトークです
二本立てになってます

プロポーザル:https://fortee.jp/phpcon-2024/proposal/6567eb24-ac3e-4ce6-aefe-db33fdf61643

【前半】PHPで学ぶプログラミングの教訓

プログラミングには様々な原理原則が存在します
また開発者は開発をしていく上で色々な教訓を得ます
そういった生の知見には一定以上の関心があります(https://x.com/nrslib/status/1819699023713828936

本セッションではこれまで培ってきた教訓を実例ベースで解説いたします

【後半】PHPで学ぶバックエンドソフトウェアアーキテクチャ選定の勘所

ソフトウェア開発とアーキテクチャは切っても切れない関係です
しかしながら、日進月歩のシステム開発の世界ではトレンドが刻一刻と変化します
そこで本セッションでは各種アーキテクチャの解説と、どのアーキテクチャを選択するかについてお話します

# URL
YouTube: https://www.youtube.com/c/narusemi
HomePage: https://nrslib.com
Twitter: https://twitter.com/nrslib
Instagram: https://www.instagram.com/nrslib/

nrs

December 22, 2024
Tweet

More Decks by nrs

Other Decks in Programming

Transcript

  1. $counter = 1; $counter = $counter + 1; $counter =

    $counter * 2; $initialCounter = 1; $incrementedCounter = $initialCounter + 1; $doubledCounter = $incrementedCounter * 2;
  2. if (!($userAge < 18 || $userName === "")) { echo

    "Valid user"; } if ($userAge >= 18 && $userName !== "") { echo "Valid user"; }
  3. // file1.php function calculateTax($amount) { return $amount * 0.1; }

    // file2.php function calculateTax($amount) { return $amount * 0.1; }
  4. // tax_utils.php function calculateTax($amount) { return $amount * 0.1; }

    // file1.php require_once 'tax_utils.php'; echo calculateTax(100); // file2.php require_once 'tax_utils.php'; echo calculateTax(200);
  5. $taxRate = 0.1; function calculateTax($price) { return $price * 0.1;

    // ハードコードされた値 } echo calculateTax(100); $taxRate = 0.1; function calculateTax($price, $taxRate) { return $price * $taxRate; } echo calculateTax(100, $taxRate);
  6. $numberOfRecords = (int)readline("Enter the number of records: "); $data =

    []; for ($i = 0; $i < $numberOfRecords; $i++) { // 入力と処理が混在 $id = (int)readline("Enter ID for record $i: "); $name = readline("Enter name for record $i: "); $score = (int)readline("Enter score for record $i: "); $status = $score >= 50 ? 'Passed' : 'Failed'; $adjustedScore = $score + 5; $date = readline("Enter date for record $i (YYYY-MM-DD): "); $data[] = [ "id" => $id, "name" => $name, "score" => $score, "status" => $status, "adjusted_score" => $adjustedScore, "date" => $date ]; echo "ID: $id, Name: $name, Status: $status, Adjusted Score: $adjustedScore, Date: $date¥n"; }
  7. $data = []; $numberOfRecords = (int)readline("Enter the number of records:

    "); for ($i = 0; $i < $numberOfRecords; $i++) { $id = (int)readline("Enter ID for record $i: "); $name = readline("Enter name for record $i: "); $score = (int)readline("Enter score for record $i: "); $date = readline("Enter date for record $i (YYYY-MM-DD): "); $data[] = [ "id" => $id, "name" => $name, "score" => $score, "date" => $date ]; } // 処理フェーズ foreach ($data as $record) { $record['status'] = $record['score'] >= 50 ? 'Passed' : 'Failed'; $record['adjusted_score'] = $record['score'] + 5; } // 出力フェーズ foreach ($data as $record) { echo "ID: {$record['id']}, Name: {$record['name']}, Status: {$record['status']}, Adjusted Score: {$record }
  8. class A { public $b; public function __construct($b) { $this->b

    = $b; } } class B { public $a; public function __construct($a) { $this->a = $a; } } $a = new A(null); $b = new B($a); $a->b = $b; // 相互参照
  9. class BankAccount { private $balance; public function __construct($balance) { $this->balance

    = $balance; } public function getBalance() { return $this->balance; } public function setBalance($balance) { $this->balance = $balance; } } $account = new BankAccount(100); if ($account->getBalance() >= 50) { $account->setBalance($account->getBalance() - 50); }
  10. class BankAccount { private $balance; public function __construct($balance) { $this->balance

    = $balance; } public function withdraw($amount) { if ($this->balance >= $amount) { $this->balance -= $amount; } else { throw new Exception("Insufficient funds"); } } } $account = new BankAccount(100); $account->withdraw(50);
  11. class SafeFileHandler { private $filePath; private $isReadOnly = true; public

    function __construct(string $filePath) { if (!file_exists($filePath)) { throw new Exception("File does not exist: $filePath"); } $this->filePath = $filePath; } public function setWritable(): void { $this->isReadOnly = false; } public function write(string $data): void { if ($this->isReadOnly) { throw new Exception("File is in read-only mode. Enable write mode explicitly using setWritable()."); } file_put_contents($this->filePath, $data, FILE_APPEND); } public function read(): string { return file_get_contents($this->filePath); } }
  12. try { $fileHandler = new SafeFileHandler("example.txt"); echo "File content:¥n" .

    $fileHandler->read(); // 書き込みモードを明示的に設定しない限り、書き込みは失敗 $fileHandler->write("New data¥n"); // 例外をスロー } catch (Exception $e) { echo "Error: " . $e->getMessage() . "¥n"; } try { $fileHandler->setWritable(); $fileHandler->write("New data¥n"); // 成功 echo "Data written successfully.¥n"; } catch (Exception $e) { echo "Error: " . $e->getMessage() . "¥n"; }
  13. class Logger { public function log($message) { echo "[Log]: $message¥n";

    } } // データベースサービスが Logger を継承 class DatabaseService extends Logger { public function query($sql) { $this->log("Executing query: $sql"); // 実際のクエリ実行(疑似コード) echo "Query result for: $sql¥n"; } } // 使用例 $dbService = new DatabaseService(); $dbService->query("SELECT * FROM users");
  14. interface LoggerInterface { public function log($message); } class ConsoleLogger implements

    LoggerInterface { public function log($message) { echo "[Console Log]: $message¥n"; } } class DatabaseService { private $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function query($sql) { $this->logger->log("Executing query: $sql"); // 実際のクエリ実行(疑似コード) echo "Query result for: $sql¥n"; } }
  15. class User { public function saveToDatabase() { // Save user

    to database } public function sendWelcomeEmail() { // Send email to user } }
  16. class User { public $name; public function __construct($name) { $this->name

    = $name; } } class UserRepository { public function save(User $user) { // Save user to database } } class EmailService { public function sendWelcomeEmail(User $user) { // Send email to user } }
  17. class DiscountService { public function calculateDiscount($customerType, $amount) { if ($customerType

    === 'Regular') { return $amount * 0.9; // 10% 割引 } elseif ($customerType === 'VIP') { return $amount * 0.8; // 20% 割引 } else { return $amount; // 割引なし } } }
  18. interface DiscountStrategy { public function calculate($amount); } class RegularDiscount implements

    DiscountStrategy { public function calculate($amount) { return $amount * 0.9; // 10% 割引 } } class VIPDiscount implements DiscountStrategy { public function calculate($amount) { return $amount * 0.8; // 20% 割引 } } class NoDiscount implements DiscountStrategy { public function calculate($amount) { return $amount; // 割引なし } } // 割引サービスクラス class DiscountService { private $strategy; public function __construct(DiscountStrategy $strategy) { $this->strategy = $strategy; } public function calculateDiscount($amount) { return $this->strategy->calculate($amount); } }
  19. // データが複数箇所に重複して管理されている $userName = "John Doe"; // 1箇所目 $firstName =

    "John"; // 2箇所目 $lastName = "Doe"; // 3箇所目 echo "User: $userName¥n"; echo "Full Name: $firstName $lastName¥n"; // 名前の変更が一貫しない $firstName = "Jane"; // 更新漏れ echo "User: $userName¥n"; // 出力が不整合になる echo "Full Name: $firstName $lastName¥n";
  20. class User { private $firstName; private $lastName; public function __construct($firstName,

    $lastName) { $this->firstName = $firstName; $this->lastName = $lastName; } public function getFullName() { return $this->firstName . ' ' . $this->lastName; } public function getFirstName() { return $this->firstName; } public function getLastName() { return $this->lastName; } public function setFirstName($firstName) { $this->firstName = $firstName; } public function setLastName($lastName) // 使用例 $user = new User("John", "Doe"); // データは一箇所で管理 echo "User: " . $user->getFullName() . "¥n"; // 名前を変更するとすべてが一貫性を保つ $user->setFirstName("Jane"); echo "User: " . $user->getFullName() . "¥n";
  21. <?php spl_autoload_register(function ($class) { echo "Hello world!"; eval("class $class {}");

    // クラスが未定義の場合に動的に作成 }); new SomeClass(); // Hello world!
  22. class Email { private $to; private $from; private $subject; private

    $message; private $headers; public function __construct($to, $message, $from = '[email protected]', $subject = 'No Subject', $headers = []) { $this->to = $to; $this->from = $from; $this->subject = $subject; $this->message = $message; $this->headers = $headers; } public function send() { // ヘッダーを構築 $formattedHeaders = "From: {$this->from}¥r¥n"; foreach ($this->headers as $key => $value) { $formattedHeaders .= "$key: $value¥r¥n"; } // デフォルト値が設定されているため、最小限の情報でメールを送信可能 mail($this->to, $this->subject, $this->message, $formattedHeaders); echo "Email sent to {$this->to} with subject '{$this->subject}'¥n"; } }
  23. // 最小限の情報で使う場合(簡単なデフォルト設定が有効) $email = new Email( to: '[email protected]', message: 'Welcome

    to our service!' ); $email->send(); // 必要な部分だけ上書きする場合 $email = new Email( to: '[email protected]', message: 'Here is your daily update!', subject: 'Daily Update', headers: ['CC' => '[email protected]'] ); $email->send();
  24. 98

  25. 99

  26. 100

  27. 101

  28. 102

  29. 104

  30. 106

  31. 108

  32. 109

  33. 110

  34. 111

  35. 112

  36. 113

  37. 114

  38. 115

  39. 116

  40. 117

  41. 118

  42. 119

  43. 120

  44. 121

  45. 122

  46. 123

  47. 124

  48. 125

  49. 126

  50. 127

  51. 128

  52. 129

  53. 130

  54. 131

  55. 132

  56. 133

  57. 134

  58. 135

  59. 136 class UserController { private UserService $userService; public function __construct(UserService

    $userService) { $this->userService = $userService; } public function createUser(string $id, string $name): void { $this->userService->createUser($id, $name); echo "User created successfully!¥n"; } public function getUser(string $id): void { $user = $this->userService->getUser($id); if ($user) { echo "User ID: " . $user->getId() . ", Name: " . $user->getName() . "¥n"; } else { echo "User not found.¥n"; } } }
  60. 137

  61. 138 class UserService { private UserRepository $userRepository; public function __construct(UserRepository

    $userRepository) { $this->userRepository = $userRepository; } public function getUser(string $id): ?User { return $this->userRepository->findById($id); } public function createUser(string $id, string $name): void { $user = new User($id, $name); $this->userRepository->save($user); } }
  62. 139

  63. 141

  64. 142 class InMemoryUserRepository implements UserRepository { private array $users =

    []; public function findById(string $id): ?User { return $this->users[$id] ?? null; } public function save(User $user): void { $this->users[$user->getId()] = $user; } }
  65. 151

  66. 152

  67. 153

  68. 157

  69. 158

  70. 161

  71. 165

  72. 172

  73. 193 Controller Presenter Use Case Interactor Use Case Input Port

    Use Case Output Port < I > < I > もっと細かく
  74. 199

  75. 200

  76. 201

  77. 202 class UserController extends BaseController { public function index(UserGetListUseCaseInterface $interactor)

    { $request = new UserGetListRequest(1, 10); $interactor->handle($request); } public function create(UserCreateUseCaseInterface $interactor, Request $request) { $name = $request->input('name'); $request = new UserCreateRequest($name); $interactor->handle($request); } }
  78. 203 class UserController extends BaseController { public function index(UserGetListUseCaseInterface $interactor)

    { $request = new UserGetListRequest(1, 10); $interactor->handle($request); } public function create(UserCreateUseCaseInterface $interactor, Request $request) { $name = $request->input('name'); $request = new UserCreateRequest($name); $interactor->handle($request); } } アプリケーションが要求するデータに入力を変換
  79. 204 class UserController extends BaseController { public function index(UserGetListUseCaseInterface $interactor)

    { $request = new UserGetListRequest(1, 10); $interactor->handle($request); } public function create(UserCreateUseCaseInterface $interactor, Request $request) { $name = $request->input('name'); $request = new UserCreateRequest($name); $interactor->handle($request); } } アプリケーションが要求するデータに入力を変換
  80. 205

  81. 206

  82. 208 DS : Data Structure class UserCreateRequest { /** *

    @var string */ private $name; /** * UserCreateRequest constructor. * @param string $name */ public function __construct(string $name) { $this->name = $name; } /** * @return string */ public function getName(): string { return $this->name; } }
  83. 209

  84. 210

  85. 211 interface UserCreateUseCaseInterface { /** * @param UserCreateRequest $request *

    @return UserCreateResponse */ public function handle(UserCreateRequest $request); } 名称は好みで InputPort, UseCase, InputBoundary などのバリエーションがあるイメージ
  86. 212

  87. 213

  88. 214 class UserCreateInteractor implements UserCreateUseCaseInterface { /** * @var UserRepositoryInterface

    */ private $userRepository; /** * @var UserCreatePresenter */ private $presenter; /** * UserCreateInteractor constructor. * @param UserRepositoryInterface $userRepository * @param UserCreatePresenterInterface $presenter */ public function __construct(UserRepositoryInterface $userRepository, UserCreatePresenterInterface $presenter) { $this->userRepository = $userRepository; $this->presenter = $presenter; } /** * @param UserCreateRequest $request * @return void */ public function handle(UserCreateRequest $request) { $userId = new UserId(uniqid()); $userName = $request->getName(); $createdUser = new User($userId, $userName); $this->userRepository->save($createdUser); $response = new UserCreateResponse($userId->getValue(), $userName); $this->presenter->output($response); } }
  89. 215

  90. 216

  91. 217 interface UserRepositoryInterface { /** * @param User $user *

    @return mixed */ public function save(User $user); /** * @param UserId $id * @return User */ public function find(UserId $id); /** * @param int $page * @param int $size * @return mixed */ public function findByPage($page, $size); }
  92. 218 interface UserRepositoryInterface { /** * @param User $user *

    @return mixed */ public function save(User $user); /** * @param UserId $id * @return User */ public function find(UserId $id); /** * @param int $page * @param int $size * @return mixed */ public function findByPage($page, $size); }
  93. 219 interface UserRepositoryInterface { /** * @param User $user *

    @return mixed */ public function save(User $user); /** * @param UserId $id * @return User */ public function find(UserId $id); /** * @param int $page * @param int $size * @return mixed */ public function findByPage($page, $size); } Gateway
  94. 220

  95. 221

  96. 222 実装は SQL 直打ちでも Entity の再構築が できれば なんでも OK class

    UserRepository implements UserRepositoryInterface { /** * @param User $user * @return mixed */ public function save(User $user) { DB::table('users') ->updateOrInsert( ['id' => $user->getId()], ['name' => $user->getName()] ); } /** * @param UserId $id * @return User */ public function find(UserId $id) { $user = DB::table('users')->where('id', $id->getValue())->first(); return new User($id, $user->name); } /** * @param int $page * @param int $size * @return mixed */ public function findByPage($page, $size) { // TODO: Implement findByPage() method. } }
  97. 223

  98. 224

  99. 225 class User { /** * @var UserId */ private

    $id; /** * @var string */ private $name; /** * User constructor. * @param UserId $id * @param string $name */ public function __construct(UserId $id, string $name) { $this->id = $id; $this->name = $name; } /** * @return UserId */ public function getId(): UserId { return $this->id; } /** * @return string */ public function getName(): string { return $this->name; } }
  100. 226

  101. 227

  102. 228 class UserCreateResponse { /** * @var string */ private

    $createdUserId; /** * @var string */ private $userName; /** * UserCreateResponse constructor. * @param string $createdUserId * @param string $userName */ public function __construct(string $createdUserId, string $userName) { $this->createdUserId = $createdUserId; $this->userName = $userName; } /** * @return string */ public function getCreatedUserId(): string { return $this->createdUserId; } /** * @return string */ public function getUserName(): string { return $this->userName; } }
  103. 229

  104. 230

  105. 231 interface UserCreatePresenterInterface { /** * @param UserCreateResponse $outputData *

    @return mixed */ public function output(UserCreateResponse $outputData); }
  106. 232

  107. 233

  108. 234 class UserCreatePresenter implements UserCreatePresenterInterface { /** * @var CleanArchitectureMiddleware

    */ private $middleware; /** * UserCreatePresenter constructor. * @param CleanArchitectureMiddleware $middleware */ public function __construct(CleanArchitectureMiddleware $middleware) { $this->middleware = $middleware; } public function output(UserCreateResponse $outputData) { $viewModel = new UserCreateViewModel($outputData->getCreatedUserId(), $outputData->getUserName()); $this->middleware->setData(view('user.create', compact('viewModel'))); } }
  109. 235

  110. 236

  111. 237 class UserCreateViewModel { /** * @var string */ private

    $id; /** * @var string */ private $name; /** * UserCreateViewModel constructor. * @param string $id * @param string $name */ public function __construct(string $id, string $name) { $this->id = $id; $this->name = $name; } /** * @return string */ public function getId(): string { return $this->id; } /** * @return string */ public function getName(): string { return $this->name; } }