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

Achieving wise architecture with Symfony2 @ SPUG#5

Achieving wise architecture with Symfony2 @ SPUG#5

Wojciech Sznapka

November 26, 2013
Tweet

More Decks by Wojciech Sznapka

Other Decks in Programming

Transcript

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

    View Slide

  2. Cześć!

    View Slide

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

    View Slide

  4. What is
    architecture?

    View Slide

  5. High level structure
    off the system

    View Slide

  6. Components
    abstractions
    connectors

    View Slide

  7. Understanding a
    system in its
    environment

    View Slide

  8. How can
    architecture
    be bad?

    View Slide

  9. Mixed
    responsibilities

    View Slide

  10. Poor separations of
    concerns

    View Slide

  11. Overloading servers
    due to an inefficient
    solutions

    View Slide

  12. Inability to
    scale

    View Slide

  13. Changes
    costs fortune

    View Slide

  14. Make it
    wisely
    with Symfony2

    View Slide

  15. Most popular
    framework in the
    PHP ecosystem

    View Slide

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

    View Slide

  17. Built on top of
    best design patterns

    View Slide

  18. Universal rules
    to get system's
    design better

    View Slide

  19. Rule #1:
    SOLID

    View Slide

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

    View Slide

  21. Each service should have
    exactly one
    purpose

    View Slide

  22. Side responsibilities
    should be
    placed in services
    and
    injected as
    dependencies

    View Slide

  23. 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;
    }
    }

    View Slide

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

    View Slide

  25. Services should be
    closed for modification
    but opened for extension

    View Slide

  26. Extending system
    should not affect
    current behavior

    View Slide

  27. Idea 1.
    inheritance

    View Slide

  28. 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;
    }
    }

    View Slide

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

    View Slide

  30. 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

    View Slide

  31. Idea 2.
    Dependency Injection
    Container
    tags

    View Slide

  32. 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');
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. 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}

    View Slide

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

    View Slide

  38. It should be possible to
    replace class with other
    implementation

    View Slide

  39. The derived class
    should not change
    parent interface

    View Slide

  40. ##############
    # 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

    View Slide

  41. Interfaces should be
    fine grained

    View Slide

  42. Many specific interfaces
    are better than one
    general­purpose

    View Slide

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

    View Slide

  44. Application should be
    decoupled into modules
    which depend upon
    abstractions

    View Slide

  45. Do not depend
    upon concretions

    View Slide

  46. Type­hint methods
    with interfaces and
    abstract classes

    View Slide

  47. Rule #2:
    Testability

    View Slide

  48. Automated testing
    is a must have!

    View Slide

  49. Unit testing is
    quite easy
    if architecture is SOLID

    View Slide

  50. Only testing in
    isolation
    makes sens!

    View Slide

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

    View Slide

  52. 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'));
    }
    }

    View Slide

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

    View Slide

  54. Rule #3:
    Scalability

    View Slide

  55. Forget about
    local disk!

    View Slide

  56. Keep sessions in
    global storage
    (memcached?)

    View Slide

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

    View Slide

  58. Write logs to
    central logger
    like syslogd ...

    View Slide

  59. … or logstash
    using GELF protocol
    supported by Monolog

    View Slide

  60. Scale your DB with
    master/slave connections
    supported by Doctrine

    View Slide

  61. Wise
    architecture
    Ingredients
    (summary)

    View Slide

  62. Design based on
    SOLID principles

    View Slide

  63. Code covered with
    automatic tests

    View Slide

  64. Testable and
    verbose (logging)
    application

    View Slide

  65. Ready to
    scale horizontally

    View Slide

  66. Dziękuję!

    View Slide

  67. We are hiring!

    View Slide

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

    View Slide