AM MAIN HOLGER WOLTERSDORF HISTORY 14 CONTROLLER ๏ THE MVC PATTERN …CONTROLLER RESPONSIBILITIES ๏ VALIDATE USER INPUT ๏ FIND MODEL(S), REPO(S) HELPER(S) OR SERVICE(S) ๏ ISSUE CHANGES ๏ EVALUATE CHANGE RESULT ๏ FIND MODEL(S), REPO(S) HELPER(S) OR SERVICE(S) ๏ QUERY AND FETCH DATA ๏ RENDER VIEW (2) ๏ RESPOND TO CLIENT
AM MAIN HOLGER WOLTERSDORF HISTORY 15 CONTROLLER ๏ THE MVC PATTERN …CONTROLLER RESPONSIBILITIES ๏ VALIDATE USER INPUT ๏ FIND MODEL(S), REPO(S) HELPER(S) OR SERVICE(S) ๏ ISSUE CHANGES ๏ EVALUATE CHANGE RESULT ๏ FIND MODEL(S), REPO(S) HELPER(S) OR SERVICE(S) ๏ QUERY AND FETCH DATA ๏ RENDER VIEW (2) ๏ RESPOND TO CLIENT WRITE READ
AM MAIN HOLGER WOLTERSDORF HISTORY 20 ๏ THE CQRS PATTERN (respecting HTTP) WRITE REQUESTHANDLER APP STATE USER INPUT MODIFIES QUERIES POST CLIENT / BROWSER REDIRECT TO PAGE 2 GET READ REQUESTHANDLER PAGE 1 PAGE 2 RESPONDS
AM MAIN HOLGER WOLTERSDORF PROJECTOR CHANGE PAGE 2 HISTORY 21 ๏ OPTIMISATION FOR READ INTENSIVE APPLICATIONS WRITE REQUESTHANDLER APP STATE USER INPUT MODIFIES POST CLIENT / BROWSER REDIRECT TO PAGE 2 GET PAGE 1 PAGE 2 CHANGE MESSAGE (ASYNC) PROJECTOR PREPARES PUB SUB QUERIES
AM MAIN HOLGER WOLTERSDORF HISTORY 22 […] the convention has been established that the GET and HEAD methods SHOULD NOT have the significance of taking an action other than retrieval. These methods ought to be considered "safe". https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
AM MAIN HOLGER WOLTERSDORF HISTORY 23 ๏ RULES SUMMARISED (FOR WEBSITES) ๏ A POST request MUST NOT not respond with UI content ๏ A POST request SHOULD change the application state ๏ A POST request SHOULD respond with a redirect ๏ A GET request SHOULD respond with UI content representing the current application state ๏ A GET request MUST NOT change the application state (read-only)
AM MAIN HOLGER WOLTERSDORF FACTS 28 ๏ MAIN COMPONENT: icehawk/icehawk 1.533 Logical lines of code (LLOC) 0 composer dependencies 0 third-party php extension dependencies v2.1.0 current stable v7.x.x php compatibility (only) STRICT typed MIT license FULLY documented at icehawk.github.io LOC Lines of code 1965 Logical lines of code 1533 Comment lines of code 433 Average volume 71.86 Average comment weight 21.28 Average intelligent content 21.28 Logical lines of code by class 29 Logical lines of code by method 5 Object oriented programming Classes 52 Interface 33 Methods 279 Methods by class 5.37 Lack of cohesion of methods 0.62 Average afferent coupling 0.66 Average efferent coupling 1.15 Average instability 0.61 Complexity Average Cyclomatic complexity by class 1.65 Average Relative system complexity 17.92 Average Difficulty 2.6
AM MAIN HOLGER WOLTERSDORF FACTS 29 ๏ OBJECTIVES ๏ Producing comprehensible and readable code ๏ Avoiding inline comments where possible (includes PHPDoc) ๏ Applying the SOLID and CLEAN CODE principles ๏ Covering all code with tests ๏ Refactoring continuously ๏ Targeting the latest PHP version ๏ Doing open source ๏ APPLICABLE FOR ๏ WEBSITES ๏ (RESTful) APIs ๏ MICRO SERVICES ๏ EVENT SOURCING
AM MAIN HOLGER WOLTERSDORF FACTS 31 POST PUT PATCH DELETE GET HEAD OPTIONS WRITE REQUEST HANDLER READ REQUEST HANDLER AUTO-RESPONSE BASIC ROUTING BEHAVIOUR
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE 35 class IceHawk { public function __construct( $config, $delegate ) public function init() {} public function handleRequest() {} }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - DELEGATION 38 final class IceHawkDelegate implements SetsUpEnvironment { public function setUpGlobalVars() { # Change your global vars $_SERVER, $_GET, $_POST, etc. # here, before IceHawk will use them. } public function setUpErrorHandling( ProvidesRequestInfo $requestInfo ) { # PHP's default error handling is used unless you set up # something else here. } public function setUpSessionHandling( ProvidesRequestInfo $requestInfo ) { # PHP's default session handling is used unless you set up # something else here. } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - DELEGATION 39 final class IceHawkDelegate implements SetsUpEnvironment { public function setUpGlobalVars() { # Change your global vars $_SERVER, $_GET, $_POST, etc. # here, before IceHawk will use them. } public function setUpErrorHandling( ProvidesRequestInfo $requestInfo ) { # PHP's default error handling is used unless you set up # something else here. } public function setUpSessionHandling( ProvidesRequestInfo $requestInfo ) { # PHP's default session handling is used unless you set up # something else here. } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - DELEGATION 40 final class IceHawkDelegate implements SetsUpEnvironment { public function setUpGlobalVars() { # Change your global vars $_SERVER, $_GET, $_POST, etc. # here, before IceHawk will use them. } public function setUpErrorHandling( ProvidesRequestInfo $requestInfo ) { # PHP's default error handling is used unless you set up # something else here. } public function setUpSessionHandling( ProvidesRequestInfo $requestInfo ) { # PHP's default session handling is used unless you set up # something else here. } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - CONFIGURATION 42 final class IceHawkConfig implements ConfiguresIceHawk { use Defaults\Traits\DefaultRequestInfoProviding; use Defaults\Traits\DefaultReadRouting; use Defaults\Traits\DefaultWriteRouting; use Defaults\Traits\DefaultRequestBypassing; use Defaults\Traits\DefaultEventSubscribing; use Defaults\Traits\DefaultCookieProviding; use Defaults\Traits\DefaultFinalReadResponding; use Defaults\Traits\DefaultFinalWriteResponding; }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - CONFIGURATION 43 final class IceHawkConfig implements ConfiguresIceHawk { use Defaults\Traits\DefaultRequestInfoProviding; use Defaults\Traits\DefaultReadRouting; use Defaults\Traits\DefaultWriteRouting; use Defaults\Traits\DefaultRequestBypassing; use Defaults\Traits\DefaultEventSubscribing; use Defaults\Traits\DefaultCookieProviding; use Defaults\Traits\DefaultFinalReadResponding; use Defaults\Traits\DefaultFinalWriteResponding; }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - CONFIGURATION 44 final class MyHawkConfig extends IceHawkConfig { public function getReadRoutes() { return [ new ReadRoute( new Literal( '/' ), new SayHelloRequestHandler() ), ]; } public function getWriteRoutes() { return [ new WriteRoute( new Literal( '/do-something' ), new DoSomethingRequestHandler() ), ]; } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - CONFIGURATION 45 # Literal route new ReadRoute( new Literal( '/' ), new SayHelloRequestHandler() ); # RegExp route with parameters from matches new ReadRoute( new RegExp("#^/post/([0-9]+)/?$#i", ['postId'] ), new ShowBlogPostRequestHandler() ); # NamedRegExp route with named matches new ReadRoute( new NamedRegExp("^/post/(?<postId>[0-9]+)/?$", 'i'), new ShowBlogPostRequestHandler() );
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - CONFIGURATION 46 # Literal route new ReadRoute( new Literal( '/' ), new SayHelloRequestHandler() ); # RegExp route with parameters from matches new ReadRoute( new RegExp("#^/post/([0-9]+)/?$#i", ['postId'] ), new ShowBlogPostRequestHandler() ); # NamedRegExp route with named matches new ReadRoute( new NamedRegExp("^/post/(?<postId>[0-9]+)/?$", 'i'), new ShowBlogPostRequestHandler() );
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - CONFIGURATION 47 # Literal route new ReadRoute( new Literal( '/' ), new SayHelloRequestHandler() ); # RegExp route with parameters from matches new ReadRoute( new RegExp("#^/post/([0-9]+)/?$#i", ['postId'] ), new ShowBlogPostRequestHandler() ); # NamedRegExp route with named matches new ReadRoute( new NamedRegExp("^/post/(?<postId>[0-9]+)/?$", 'i'), new ShowBlogPostRequestHandler() );
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - CONFIGURATION 48 final class MyHawkConfig extends IceHawkConfig { public function getReadRoutes() { $routes = require(__DIR__ . '../config/ReadRoutes.php'); # $routes = [ # "^/post/(?<postId>[0-9]+)/?" # => MyVendor\...\ShowPostRequestHandler::class, # ]; foreach ($routes as $pattern => $handlerClass) { yield new ReadRoute( new NamedRegExp($pattern), new $handlerClass() ), } } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - REQUEST BYPASSING 50 final class IceHawkConfig implements ConfiguresIceHawk { use Defaults\Traits\DefaultRequestInfoProviding; use Defaults\Traits\DefaultReadRouting; use Defaults\Traits\DefaultWriteRouting; use Defaults\Traits\DefaultRequestBypassing; use Defaults\Traits\DefaultEventSubscribing; use Defaults\Traits\DefaultCookieProviding; use Defaults\Traits\DefaultFinalReadResponding; use Defaults\Traits\DefaultFinalWriteResponding; }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - REQUEST BYPASSING 51 final class MyHawkConfig extends IceHawkConfig { public function getRequestBypasses() { return [ new RequestBypass( new Literal('/came/via/get'), '/do/some/write/action', HttpMethod::POST ), ]; } public function getWriteRoutes() { return [ new WriteRoute( new Literal( '/do/some/write/action' ), new DoSomethingRequestHandler() ), ]; } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - REQUEST BYPASSING 52 final class MyHawkConfig extends IceHawkConfig { public function getRequestBypasses() { return [ new RequestBypass( new Literal('/came/via/get'), '/do/some/write/action', HttpMethod::POST ), ]; } public function getWriteRoutes() { return [ new WriteRoute( new Literal( '/do/some/write/action' ), new DoSomethingRequestHandler() ), ]; } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - REQUEST HANDLERS 54 final class SayHelloRequestHandler implements HandlesGetRequest { public function handle(ProvidesReadRequestData $request) { $info = $request->getInfo(); $input = $request->getInput(); $cookies = $request->getCookies(); (new Page())->respond('Hello!', HttpCode::OK); } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - REQUEST HANDLERS 55 final class DoSomethingRequestHandler implements HandlesPostRequest { public function handle(ProvidesWriteRequestData $request) { $info = $request->getInfo(); $input = $request->getInput(); $cookies = $request->getCookies(); $files = $input->getAllUploadedFiles(); (new Redirect())->respond('/done'); } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - EVENTS & SUBSCRIBERS 57 final class IceHawkConfig implements ConfiguresIceHawk { use Defaults\Traits\DefaultRequestInfoProviding; use Defaults\Traits\DefaultReadRouting; use Defaults\Traits\DefaultWriteRouting; use Defaults\Traits\DefaultRequestBypassing; use Defaults\Traits\DefaultEventSubscribing; use Defaults\Traits\DefaultCookieProviding; use Defaults\Traits\DefaultFinalReadResponding; use Defaults\Traits\DefaultFinalWriteResponding; }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - EVENTS & SUBSCRIBERS 58 final class MyHawkConfig extends IceHawkConfig { public function getEventSubscribers() : array { return [ new LogSubscriber(), ]; } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - EVENTS & SUBSCRIBERS 59 final class LogSubscriber extends AbstractEventSubscriber { private $startTime; public function getAcceptedEvents() : array { return [ IceHawk\IceHawk\Events\HandlingReadRequestEvent::class, IceHawk\IceHawk\Events\ReadRequestWasHandledEvent::class, ]; } public function whenHandlingReadRequest(HandlingReadRequestEvent $event) { $this->startTime = microtime(true); } public function whenReadRequestWasHandled(ReadReqeustWasHandledEvent $event) { error_log( sprintf( "Your app took %f seconds to handle the request on URI: %s", (microtime(true) - $this->startTime), $event->getRequestInfo()->getUri() ), 3, 'file.log' ); } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - FINAL RESPONDING 61 final class IceHawkConfig implements ConfiguresIceHawk { use Defaults\Traits\DefaultRequestInfoProviding; use Defaults\Traits\DefaultReadRouting; use Defaults\Traits\DefaultWriteRouting; use Defaults\Traits\DefaultRequestBypassing; use Defaults\Traits\DefaultEventSubscribing; use Defaults\Traits\DefaultCookieProviding; use Defaults\Traits\DefaultFinalReadResponding; use Defaults\Traits\DefaultFinalWriteResponding; }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - FINAL RESPONDING 62 final class MyHawkConfig extends IceHawkConfig { public function getFinalReadResponder() : RespondsFinallyToReadRequest { return MyFinalReadResponder(); } }
AM MAIN HOLGER WOLTERSDORF FEATURES & CODE - FINAL RESPONDING 63 final class MyFinalReadResponder implements RespondsFinallyToReadRequest { public function handleUncaughtException( \Throwable $throwable, ProvidesReadReqeustData $request ) { try { throw $throwable; } catch (UnresolvedRequest $e) { (new NotFound())->respond(); } catch (\Throwable $e) { (new InternalServerError())->respond(); } } }
AM MAIN HOLGER WOLTERSDORF SESSION - EXAMPLE IMPLEMENTATON 67 final class Session extends AbstractSession { const KEY_SOME_STRING_VALUE = 'someStringValue'; public function getSomeStringValue() : string { return $this->get( self::KEY_SOME_STRING_VALUE ); } public function setSomeStringValue( string $value ) { $this->set( self::KEY_SOME_STRING_VALUE, $value ); } public function isSomeStringValueSet() : bool { return $this->isset( self::KEY_SOME_STRING_VALUE ); } public function unsetSomeStringValue() { $this->unset( self::KEY_SOME_STRING_VALUE ); } }
AM MAIN HOLGER WOLTERSDORF SESSION - DATA MAPPER 68 final class MyDataMapper implements MapsSessionData { public function toSessionData( $value ) { return base64_encode( $value ); } public function fromSessionData( $sessionData ) { return base64_decode( $sessionData ); } }
AM MAIN HOLGER WOLTERSDORF SESSION - REGISTER DATA MAPPERS 69 # Add the data mapper for all keys in the registry $session->addDataMapper( new MyDataMapper() ); # Add the data mapper for one specific key in the registry $session->addDataMapper( new MyDataMapper(), [Session::KEY_SOME_STRING_VALUE] ); # Add the data mapper for multiple keys in the registry $session->addDataMapper( new MyDataMapper(), [ Session::KEY_SOME_STRING_VALUE, Session::KEY_SOME_OTHER_VALUE ] );
AM MAIN HOLGER WOLTERSDORF FORMS 71 ๏ DTO TO BE STORED IN SESSION ๏ CSRF protection using tokens (with expiration) ๏ Data pre-setting ๏ Gets input validation feedback (with severity) on write side ๏ Provides input validation feedback on read side
AM MAIN HOLGER WOLTERSDORF FORMS - READ SIDE INITIALISATION 72 $form = new Form( new FormId('profile') ); $form->renewToken(); if ( !$form->wasDataSet() ) { # Set up some default data (single) $form->set( 'username', 'johndoe' ); }
AM MAIN HOLGER WOLTERSDORF FORMS - WRITE SIDE 73 $form = $session->getForm( new FormId('profile') ); $token = Token::fromString($input->get('token')); $form->guardTokenIsValid($token); $form->addFeedback( 'user', new Feedback( 'Invalid username', Feedback::ERROR ) );
AM MAIN HOLGER WOLTERSDORF PUBSUB 75 ๏ BASIC PUBLISH SUBSCRIBE SYSTEM TO DECOUPLE BUSINESS LOGIC ๏ Defines interfaces for messages, channels and subscribers ๏ Provides a message bus for publishing and subscribing ๏ Provides abstract message subscriber with auto-method resolving
AM MAIN HOLGER WOLTERSDORF PUBSUB - A MESSAGE 76 final class YourMessage implements CarriesInformation { public function getMessageId() : IdentifiesMessage { return new MessageId('message-xyz'); } public function getMessageName() : NamesMessage { return new MessageName('User email address changed'); } public function getContent() : string { return '[email protected]'; } }
AM MAIN HOLGER WOLTERSDORF PUBSUB - A SUBSCRIBER 77 final class YourSubscriber extends AbstractMessageSubscriber { protected function whenSomethingHadHappened( YourMessage $yourMessage, Channel $channel ) { printf( 'Message named "%s" with ID "%s" ' . 'was published on channel "%" with content: "%s"', $yourMessage->getMessageName(), $yourMessage->getMessageId(), $channel, $yourMessage->getContent() ); } }
AM MAIN HOLGER WOLTERSDORF PUBSUB - THE SUBSCRIPTION 78 $messageBus = new MessageBus(); $messageBus->subscribe( new Channel('ListenToMe'), new YourSubscriber() );