Slide 1

Slide 1 text

Railway Oriented Programming in PHP on practice Yevhen Kuzminov

Slide 2

Slide 2 text

MobiDev:/$ whoami Yevhen Kuzminov |> Team Leader |> PHP 2009 |> Ruby 2014 |> Elixir 2016

Slide 3

Slide 3 text

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!

Slide 4

Slide 4 text

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); }

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

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);

Slide 7

Slide 7 text

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);

Slide 8

Slide 8 text

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);

Slide 9

Slide 9 text

What if you could this, while still handling errors? //pseudocode $request |> castRequest |> validateRequest |> updateDB |> sendNotification |> writeLog |> render

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Happy vs Unhappy path Cast Validate Update Request Response Cast Validate Update Request Response Errors vs

Slide 12

Slide 12 text

Two-ways path Cast Validate Update Request Response Errors Issue: One-track input, but Two-track output. How to compose/chain such functions?

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Monad is the answer! it is just a monoid in the category of endofunctors... Well maybe not

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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 ]; }

Slide 18

Slide 18 text

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); } }

Slide 19

Slide 19 text

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()); } }

Slide 20

Slide 20 text

Operation Railway Steps ● step ● failure ● always ● tryCatch ● rawStep (Custom Step Class)

Slide 21

Slide 21 text

Step options (flow control) ->step('castRequest', ['name' => 'Cast', 'before' => 'Find']) ● name ● before ● after ● replace ● failFast And special Operation method “removeStep” ->removeStep('StepName')

Slide 22

Slide 22 text

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); }

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

More examples einfach-operation on GitHub https://github.com/iJackUA/einfach-operation

Slide 31

Slide 31 text

Questions? [email protected] http://stdout.in @iJackUA