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

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

More Decks by Yevhen "Eugene" Kuzminov

Other Decks in Programming

Transcript

  1. 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!
  2. 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); }
  3. 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);
  4. 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);
  5. 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);
  6. What if you could this, while still handling errors? //pseudocode

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

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

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

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

    the category of endofunctors... Well maybe not
  11. 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
  12. 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
  13. 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 ]; }
  14. 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); } }
  15. 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()); } }
  16. Operation Railway Steps • step • failure • always •

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

    'Find']) • name • before • after • replace • failFast And special Operation method “removeStep” ->removeStep('StepName')
  18. 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); }
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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