Railway Oriented Programming in PHP

Railway Oriented Programming in PHP

MobiDev Meetup, Kharkiv, Ukraine http://meetup.mobidev.com.ua/

Code examples https://github.com/iJackUA/einfach-operation

Transcript

  1. Railway Oriented Programming in PHP on practice Yevhen Kuzminov

  2. MobiDev:/$ whoami Yevhen Kuzminov |> Team Leader |> PHP 2009

    |> Ruby 2014 |> Elixir 2016
  3. Who inspired me to investigate RoP • Nick Sutterer (https://twitter.com/apotonick)

    ◦ http://trailblazer.to • Scott Wlaschin (http://fsharpforfunandprofit.com) ◦ https://www.slideshare.net/ScottWlaschin/railway-oriented-progra mming • My Haskell-guru friend Stas!
  4. Sweet careless PHP code... function doTheJob($request) { $castRequest = castRequest($request);

    $validRequest = validateRequest($castRequest); $dbResult = updateDB($validRequest); sendNotification($dbResult, $validRequest); writeLog($dbResult, $validRequest); return render($dbResult, $validRequest); }
  5. None
  6. Bitter careful PHP code... $castRequest = castRequest($request); $validRequest = validateRequest($castRequest);

    if(!$validRequest) { return 'Request is invalid'; } $dbResult = updateDB($validRequest); sendNotification($dbResult, $validRequest); writeLog($dbResult, $validRequest); return render($dbResult, $validRequest);
  7. Bitter careful PHP code... $castRequest = castRequest($request); $validRequest = validateRequest($castRequest);

    if(!$validRequest) { return 'Request is invalid'; } $dbResult = updateDB($validRequest); if($dbResult) { return 'DB update failed'; } sendNotification($dbResult, $validRequest); writeLog($dbResult, $validRequest); return render($dbResult, $validRequest);
  8. Bitter careful PHP code... $castRequest = castRequest($request); $validRequest = validateRequest($castRequest);

    if(!$validRequest) { return 'Request is invalid'; } $dbResult = updateDB($validRequest); if($dbResult) { return 'DB update failed'; } try { sendNotification($dbResult, $validRequest); } catch (Exception $e) { echo "Notification server connection error: {$e->message()}"; } writeLog($dbResult, $validRequest); return render($dbResult, $validRequest);
  9. What if you could this, while still handling errors? //pseudocode

    $request |> castRequest |> validateRequest |> updateDB |> sendNotification |> writeLog |> render
  10. None
  11. Happy vs Unhappy path Cast Validate Update Request Response Cast

    Validate Update Request Response Errors vs
  12. Two-ways path Cast Validate Update Request Response Errors Issue: One-track

    input, but Two-track output. How to compose/chain such functions?
  13. Monad is the answer! it is just a monoid in

    the category of endofunctors...
  14. Monad is the answer! it is just a monoid in

    the category of endofunctors... Well maybe not
  15. What if you could have such a flow? Step Cast

    Step Validate Step Update Try Catch Send Email Always Action Log Failure Error Log Railway Input Railway Output
  16. Here comes the Railway! https://github.com/iJackUA/einfach-operation $result = (new Railway) ->

    step( 'castRequest' ) -> step( 'validateRequest' ) -> step( 'updateDB' ) -> tryCatch( 'sendNotification' ) -> always( 'writeLog' ) -> failure( 'appendErrorLog' ) -> runWithParams( ['id' => 10, 'name' => 'Yevhen'] ); if ($result->isSuccess()) { print_r($result->params()); } elseif ($result->isError()) { return $result->errorsText(); } Any PHP “callable” passed to the Step http://php.net/manual/en/language.types.callable.php
  17. Making Two-track output public function castRequest($params) { $params['id'] = (int)

    $params['id']; return ok($params); } … // simplified form function ok(array $params) { return [ 'type' => RESPONSE_TYPE_OK, // or RESPONSE_TYPE_ERROR for error($params) 'params' => $params ]; }
  18. Operation - encapsulates BL with Railway class MyOperation implements \einfach\operation\IOperation

    { public function railway() : Railway { return (new Railway) ->step(function ($params) { return ok($params, ['a' => 'c']); }) ->step([$this, 'castRequest'], ['name' => 'Cast']); } public function __invoke(array $params) : Result { return $this->railway()->runWithParams($params); } public function castRequest($params) { return ok($params); } }
  19. Operation Test class OperationTest extends \PHPUnit\Framework\TestCase { public function testMyOperation()

    { $params = ['a' => 'b']; $result = (new MyOperation)($params); $this->assertTrue($result->isSuccess()); $this->assertFalse($result->isError()); $this->assertEquals($result->params()['a'], 'c'); $this->assertCount(0, $result->errors()); $this->assertEquals('', $result->errorsText()); } }
  20. Operation Railway Steps • step • failure • always •

    tryCatch • rawStep (Custom Step Class)
  21. Step options (flow control) ->step('castRequest', ['name' => 'Cast', 'before' =>

    'Find']) • name • before • after • replace • failFast And special Operation method “removeStep” ->removeStep('StepName')
  22. Nested Railway Step $result = (new Railway) ->step( 'nestedStep' )

    ->step( 'castRequest' ) ->runWithParams(['a' => 'b']); function nestedStep($params) { return (new Railway) ->step(function ($params) { return ok($params, ['nestedRwParam' => 'nestedRwValue']); }) ->runWithParams($params); }
  23. None
  24. CRUD Controller example (BL encapsulation) class CRUDController { function actionCreate($params)

    { $params['user'] = Auth::user(); $result = (new CreateOperation)($params); return $this->renderOpResult($result); } protected function renderOpResult($result) { if ($result->isSuccess()) { print_r($result->pipeline()); return $result->params()['model']; } else { print_r($result->pipeline()); return $result->errorsText(); } } } Pipeline: [0] => Step | CreateOperation::validate [1] => Step | CreateOperation::create
  25. CRUD: Create Operation class CreateOperation implements \einfach\operation\IOperation { use CRUDTraits;

    public function railway() : Railway { return (new Railway) ->step([$this, 'validate']) ->step([$this, 'create']); } … public function create($params) { $model = Repo::create($params); return ok($params, [ 'model' => $model ]); } } Pipeline: [0] => Step | CreateOperation::validate [1] => Step | CreateOperation::create
  26. CRUD: Read Operation class ReadOperation implements \einfach\operation\IOperation { use CRUDTraits;

    public function railway() : Railway { return (new Railway) ->step([$this, 'checkPermissions']) ->step([$this, 'getArticle'], ['name' => 'get']); } … public function getArticle($params) { $article = Repo::find($params['id']); return ok($params, [ 'model' => $article ]); } } Pipeline: [0] => Step | ReadOperation::checkPermissions [1] => Step | get
  27. CRUD: Update Operation class UpdateOperation extends ReadOperation implements \einfach\operation\IOperation {

    use CRUDTraits; public function railway() : Railway { return parent::railway() ->step([$this, 'update']); } … public function update($params) { $params['model']->price = $params['price']; $params['model']->name = $params['name']; return Repo::save($params['model']) ? ok($params) : error($params, 'Update failed'); } } Pipeline: [0] => Step | UpdateOperation::checkPermissions [1] => Step | get [2] => Step | UpdateOperation::update
  28. CRUD: Delete Operation class DeleteOperation extends ReadOperation implements \einfach\operation\IOperation {

    public function railway() : Railway { return parent::railway() ->step([$this, 'delete']); } … public function delete($params) { return Repo::save($params['model']) ? ok($params) : error($params, 'Delete failed'); } } Pipeline: [0] => Step | DeleteOperation::checkPermissions [1] => Step | get [2] => Step | DeleteOperation::delete
  29. CRUD: Admin Delete Operation class AdminDelete extends DeleteOperation implements \einfach\operation\IOperation

    { public function railway() : Railway { return parent::railway() ->failure([$this, 'trackViolation']); } ... public function checkPermissions($params) { return ( $params['user']->login == 'admin' ) ? ok($params) : error($params, 'Permission denied!'); } public function trackViolation($params) { // write var_dump($params) to error log in case of failure return ok($params); } } Pipeline: [0] => Step | AdminDelete::checkPermissions [1] => Step | get [2] => Step | AdminDelete::delete Pipeline: [0] => Step | AdminDelete::checkPermissions [1] => Failure | AdminDelete::trackViolation
  30. More examples einfach-operation on GitHub https://github.com/iJackUA/einfach-operation

  31. Questions? kyzminov@gmail.com http://stdout.in @iJackUA