Slide 1

Slide 1 text

Achieving wise architecture with Symfony2 Wojciech Sznapka Silesian PHP User Group Gliwice, 26.XI.2013

Slide 2

Slide 2 text

Cześć!

Slide 3

Slide 3 text

Wojciech Sznapka Technical Manager @ XSolve Zend Certified Engineer since 2010 Symfony user since 2008 Coding since 2004 Besides: ice hockey, windsurfing, skiing, crime stories

Slide 4

Slide 4 text

What is architecture?

Slide 5

Slide 5 text

High level structure off the system

Slide 6

Slide 6 text

Components abstractions connectors

Slide 7

Slide 7 text

Understanding a system in its environment

Slide 8

Slide 8 text

How can architecture be bad?

Slide 9

Slide 9 text

Mixed responsibilities

Slide 10

Slide 10 text

Poor separations of concerns

Slide 11

Slide 11 text

Overloading servers due to an inefficient solutions

Slide 12

Slide 12 text

Inability to scale

Slide 13

Slide 13 text

Changes costs fortune

Slide 14

Slide 14 text

Make it wisely with Symfony2

Slide 15

Slide 15 text

Most popular framework in the PHP ecosystem

Slide 16

Slide 16 text

Source: GitHub symfony/symfony, 26.XI.2013

Slide 17

Slide 17 text

Built on top of best design patterns

Slide 18

Slide 18 text

Universal rules to get system's design better

Slide 19

Slide 19 text

Rule #1: SOLID

Slide 20

Slide 20 text

ingle responsibility principle S O L I D pen/closed principle iskov substitution principle nterface segregation principle ependency inversion principle

Slide 21

Slide 21 text

Each service should have exactly one purpose

Slide 22

Slide 22 text

Side responsibilities should be placed in services and injected as dependencies

Slide 23

Slide 23 text

class MailingManager implements MailingManagerInterface { protected $templateManager; public function __construct(TemplateManagerInterface $templateManager) { $this->templateManager = $templateManager; } public function createMailing($data, Mailing $mailing) { $body = $this->templateManager->applyTemplate( $mailing->getBody(), $mailing->getTitle()); $mailing->setBody($body); $mailing->setSubject($data['subject']); return $mailing; } }

Slide 24

Slide 24 text

class TemplateManager implements TemplateManagerInterface { protected $templateContentTag = '{{ content }}'; protected $templateTitleTag = '{{ title }}'; public function applyTemplate($body, $title) { return str_replace( array($this->templateContentTag, $this->templateTitleTag), array($body, $title), $this->getActiveTemplateBody() ); } }

Slide 25

Slide 25 text

Services should be closed for modification but opened for extension

Slide 26

Slide 26 text

Extending system should not affect current behavior

Slide 27

Slide 27 text

Idea 1. inheritance

Slide 28

Slide 28 text

class ComplicatedBusinessLogic { public function __construct(DependencyOne $depOne, DependencyTwo $depTwo, DependencyThree $depThree, DependencyFour $depFour) { $this->depOne = $depOne; $this->depTwo = $depTwo; $this->depThree = $depThree; $this->depFour = $depFour; } public function performLogic($param1, $param2) { $partialResult = $this->getPartialResult($param1, $param2); // [...] do some really complicated logic return $finalResult; } protected function getPartialResult($param1, $param2) { // [...] produce partial result return $partialResult; } }

Slide 29

Slide 29 text

class SpecyficBusinessLogic extends ComplicatedBusinessLogic { protected function getPartialResult($param1, $param2) { // produce partial result in a specific way // to change whole class behavior return $partialResultAltered; } }

Slide 30

Slide 30 text

parameters: complicated_business_logic.class: ComplicatedBusinessLogic specyfic_business_logic.class: SpecyficBusinessLogic services: complicated_business_logic: class: %complicated_business_logic.class% arguments: - @dependency.one - @dependency.two - @dependency.three - @dependency.four specyfic_business_logic: class: %specyfic_business_logic.class% parent: complicated_business_logic

Slide 31

Slide 31 text

Idea 2. Dependency Injection Container tags

Slide 32

Slide 32 text

class Calculator { protected $evaluators; public function __construct() { $this->evaluators = array(); } public function registerEvaluator(EvaluatorInterface $evaluator) { $this->evaluator[] = $evaluator; } public function calculate($expression) { for ($this->evaluators as $evaluator) { if ($evaluator->accepts($expression)) { return $expression->calculate($expression); } } throw new \RuntimeException('No suitable evaluator here'); } }

Slide 33

Slide 33 text

interface EvaluatorInterface { public function accepts($expression); public function calculate($expression); }

Slide 34

Slide 34 text

class PlusEvaluator implements EvaluatorInterface { public function accepts($expression) { return strpos($expression, '+') !== false; } public function calculate($expression) { // do some calculations return $result; } }

Slide 35

Slide 35 text

class MinusEvaluator implements EvaluatorInterface { public function accepts($expression) { return strpos($expression, '-') !== false; } public function calculate($expression) { // do some calculations return $result; } }

Slide 36

Slide 36 text

parameters: plus_evaluator.class: PlusEvaluator minus_evaluator.class: MinusEvaluator calculator.class: Calculator services: calculator: class: %calculator.class% plus_evaluator: class: %plus_evaluator.class% tags: - {name: calculator.evaluator} minus_evaluator: class: %minus_evaluator.class% tags: - {name: calculator.evaluator}

Slide 37

Slide 37 text

use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Reference; class EvaluatorCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { $definition = $container->getDefinition('calculator'); $svcs = $container->findTaggedServiceIds('calculator.evaluator'); foreach ($svcs as $id => $attributes) { $definition->addMethodCall( 'registerEvaluator', array(new Reference($id)) ); } } }

Slide 38

Slide 38 text

It should be possible to replace class with other implementation

Slide 39

Slide 39 text

The derived class should not change parent interface

Slide 40

Slide 40 text

############## # services.yml ############## parameters: webservice_consumer.class: Wowo\Webservice\Consumer webservice_api_client.class: Wowo\Webservice\ApiClient services: webservice_consumer: class: %webservice_consumer.class% arguments: - @webservice_api_client webservice_api_client: class: %webservice_api_client.class% ################ # config_dev.yml ################ parameters: webservice_api_client: Wowo\Mocks\FakeDataMockProvider

Slide 41

Slide 41 text

Interfaces should be fine grained

Slide 42

Slide 42 text

Many specific interfaces are better than one general­purpose

Slide 43

Slide 43 text

interface PlaceholderProcessorInterface { public function process($object, $body); } interface TemplateManagerInterface { public function setAvailableTemplates(array $templates); public function getAvailableTemplates(); public function applyTemplate($body, $title); } interface MediaManagerInterface { public function embed($body, \Swift_Message $message); public function getRegex($name); } interface BuilderInterface { public function buildMessage($mailingId, $contactId, $contactClass); } interface SenderInterface { public function send($mailingId, $contactId, $contactClass); }

Slide 44

Slide 44 text

Application should be decoupled into modules which depend upon abstractions

Slide 45

Slide 45 text

Do not depend upon concretions

Slide 46

Slide 46 text

Type­hint methods with interfaces and abstract classes

Slide 47

Slide 47 text

Rule #2: Testability

Slide 48

Slide 48 text

Automated testing is a must have!

Slide 49

Slide 49 text

Unit testing is quite easy if architecture is SOLID

Slide 50

Slide 50 text

Only testing in isolation makes sens!

Slide 51

Slide 51 text

class WebserviceConsumer { protected $client; protected $logger public function __construct(WebserviceApiClient $client, Logger $logger) { $this->client = $client; $this->logger = $logger; } public function getUser($name) { try { $this->logger->info(sprintf('Attempting get user %s', $name)); $user = $this->client->callGetUserDataFunction($name); // do something with data // wrap into suitable class // or transform it somehow $this->logger->info(sprintf('Got user data for %s', $name), array('user' => $user)); return $user; } catch (\Exception $e) { $this->logger->error(sprintf('%s occured while getting user: %s', get_class($e), $e->getMessage), array('exception' => $e)); throw new WebserviceException('GetUser error' , 0, $e); } } }

Slide 52

Slide 52 text

class WebserviceTest extends \PHPUnit_Framework_TestCase { public function tearDown() { \Mockery::close(); } public function testSuccessfulGetUser() { $testData = array('name' => 'Wojciech', 'company' => 'Xsolve'); $clientMock = \Mockery::mock('WebserviceApiClient'); $clientMock ->shouldReceive('callGetUserDataFunction') ->times(1) ->andReturn($testData); $loggerMock = \Mockery::mock('\Monolog\Logger') ->shouldReceive('info') ->times(2); $consumer = new WebserviceApiClient($clientMock, $loggerMock); $this->assertEquals($testData, $consumer->getUser('Wojciech')); } }

Slide 53

Slide 53 text

class WebserviceTest extends \PHPUnit_Framework_TestCase { public function tearDown() { \Mockery::close(); } /** @expectedException WebserviceException */ public function testFailuerWhileGettingUser() { $clientMock = \Mockery::mock('WebserviceApiClient'); $clientMock ->shouldReceive('callGetUserDataFunction') ->times(1) ->andThrow('\RuntimeException'); $loggerMock = \Mockery::mock('\Monolog\Logger') ->shouldReceive('info') ->times(1); $loggerMock ->shouldReceive('error') ->times(1); $consumer = new WebserviceApiClient($clientMock, $loggerMock); } }

Slide 54

Slide 54 text

Rule #3: Scalability

Slide 55

Slide 55 text

Forget about local disk!

Slide 56

Slide 56 text

Keep sessions in global storage (memcached?)

Slide 57

Slide 57 text

Store users' files global storage (S3/CDN?)

Slide 58

Slide 58 text

Write logs to central logger like syslogd ...

Slide 59

Slide 59 text

… or logstash using GELF protocol supported by Monolog

Slide 60

Slide 60 text

Scale your DB with master/slave connections supported by Doctrine

Slide 61

Slide 61 text

Wise architecture Ingredients (summary)

Slide 62

Slide 62 text

Design based on SOLID principles

Slide 63

Slide 63 text

Code covered with automatic tests

Slide 64

Slide 64 text

Testable and verbose (logging) application

Slide 65

Slide 65 text

Ready to scale horizontally

Slide 66

Slide 66 text

Dziękuję!

Slide 67

Slide 67 text

We are hiring!

Slide 68

Slide 68 text

Wojciech Sznapka [email protected] blog.sznapka.pl @sznapka @wowo