Top Banner
How Kris Writes Symfony Apps
106

How Kris Writes Symfony Apps

Sep 08, 2014

Download

Technology

Kris Wallsmith

You’ve seen Kris’ open source libraries, but how does he tackle coding out an application? Walk through green fields with a Symfony expert as he takes his latest “next big thing” idea from the first line of code to a functional prototype. Learn design patterns and principles to guide your way in organizing your own code and take home some practical examples to kickstart your next project.
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: How Kris Writes Symfony Apps

How Kris Writes Symfony Apps

Page 2: How Kris Writes Symfony Apps

@kriswallsmith

Page 3: How Kris Writes Symfony Apps

father artist bowhunter hacker president widower gamer actor tapdancer lover hater singer writer founder yogi consultant archer musician architect slacker soccer player volunteer home owner scotch drinker pianist…

Page 4: How Kris Writes Symfony Apps

assetic

Page 5: How Kris Writes Symfony Apps

Buzz

Page 6: How Kris Writes Symfony Apps

Spork

Page 7: How Kris Writes Symfony Apps
Page 8: How Kris Writes Symfony Apps

Getting Started

Page 9: How Kris Writes Symfony Apps

composer create-project \! symfony/framework-standard-edition \! widgets-by-kris/ \! ~2.4

Page 10: How Kris Writes Symfony Apps

- "doctrine/orm": "~2.2,>=2.2.3",!- "doctrine/doctrine-bundle": "~1.2",!+ "doctrine/mongodb-odm-bundle": "~3.0",!+ "jms/di-extra-bundle": "~1.4",!+ "jms/security-extra-bundle": "~1.5",!+ "jms/serializer-bundle": "~1.0",

Page 11: How Kris Writes Symfony Apps

./app/console generate:bundle \! --namespace=Kris/Bundle/MainBundle

Page 12: How Kris Writes Symfony Apps

public function registerContainerConfiguration(LoaderInterface $loader)!{! $loader->load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml');!! // load local_*.yml or local.yml! if (! file_exists($file = __DIR__.'/config/local_'.$this->getEnvironment().'.yml')! ||! file_exists($file = __DIR__.'/config/local.yml')! ) {! $loader->load($file);! }!}

Page 13: How Kris Writes Symfony Apps

Model

Page 14: How Kris Writes Symfony Apps

Treat your model like a princess.

Page 15: How Kris Writes Symfony Apps

She gets her own wing of the palace…

Page 16: How Kris Writes Symfony Apps

doctrine_mongodb:! auto_generate_hydrator_classes: %kernel.debug%! auto_generate_proxy_classes: %kernel.debug%! connections: { default: ~ }! document_managers:! default:! connection: default! database: kris! mappings:! model:! type: annotation! dir: %src%/Kris/Model! prefix: Kris\Model! alias: Model

Page 17: How Kris Writes Symfony Apps

// repo for src/Kris/Model/Widget.php!$repo = $this->dm->getRepository('Model:User');

Page 18: How Kris Writes Symfony Apps

…doesn't do any work…

Page 19: How Kris Writes Symfony Apps

use Kris\Bundle\MainBundle\Canonicalizer;!!public function setUsername($username)!{! $this->username = $username;!! $canonicalizer = Canonicalizer::instance();! $this->usernameCanonical = $canonicalizer->canonicalize($username);!}

Page 20: How Kris Writes Symfony Apps

use Kris\Bundle\MainBundle\Canonicalizer;!!public function setUsername($username, Canonicalizer $canonicalizer)!{! $this->username = $username;! $this->usernameCanonical = $canonicalizer->canonicalize($username);!}

Page 21: How Kris Writes Symfony Apps

…and is unaware of the work being done around her.

Page 22: How Kris Writes Symfony Apps

public function setUsername($username)!{! // a listener will update the! // canonical username! $this->username = $username;!}

Page 23: How Kris Writes Symfony Apps

Anemic domain model is an anti-pattern?

Page 24: How Kris Writes Symfony Apps

Anemic???

Page 25: How Kris Writes Symfony Apps

“The fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design;

which is to combine data and process together.” !

Martin Fowler

Page 26: How Kris Writes Symfony Apps

$cabinet->open();

Page 27: How Kris Writes Symfony Apps

Cabinets don’t open themselves.

Page 28: How Kris Writes Symfony Apps

$asset->getLastModified();

Page 29: How Kris Writes Symfony Apps

Mapping Layers

Page 30: How Kris Writes Symfony Apps

thin

Page 31: How Kris Writes Symfony Apps

thin controller fat model

Page 32: How Kris Writes Symfony Apps

MVC

Page 33: How Kris Writes Symfony Apps

Is Symfony an MVC framework?

Page 34: How Kris Writes Symfony Apps

HTTP

Page 35: How Kris Writes Symfony Apps

HT

TP Land

Application Land

Controller

Page 36: How Kris Writes Symfony Apps

The controller is thin because it maps from

HTTP-land to application-land.

Page 37: How Kris Writes Symfony Apps

What about the model?

Page 38: How Kris Writes Symfony Apps
Page 39: How Kris Writes Symfony Apps
Page 40: How Kris Writes Symfony Apps

public function registerAction()!{! // ...! $user->sendWelcomeEmail();! // ...!}

Page 41: How Kris Writes Symfony Apps

public function registerAction()!{! // ...! $mailer->sendWelcomeEmail($user);! // ...!}

Page 42: How Kris Writes Symfony Apps

Application Land

Persistence Land

Model

Page 43: How Kris Writes Symfony Apps

The model maps from application-land to persistence-land.

Page 44: How Kris Writes Symfony Apps

Model

Application Land

Persistence Land

HT

TP Land

Controller

Page 45: How Kris Writes Symfony Apps

Who lives in application land?

Page 46: How Kris Writes Symfony Apps

Thin controller, thin model… Fat service layer!

Page 47: How Kris Writes Symfony Apps

Application Events

Page 48: How Kris Writes Symfony Apps

Use them.

Page 49: How Kris Writes Symfony Apps

That happened.

Page 50: How Kris Writes Symfony Apps

/** @DI\Observe("user.username_change") */!public function onUsernameChange(UserEvent $event)!{! $user = $event->getUser();! $dm = $event->getDocumentManager();!! $dm->getRepository('Model:Widget')! ->updateDenormalizedUsernames($user);!}

Page 51: How Kris Writes Symfony Apps

One event class per model

• Event name constants

• Accepts object manager and object as arguments

• Simple accessors

Page 52: How Kris Writes Symfony Apps

$event = new UserEvent($dm, $user);!$dispatcher->dispatch(UserEvent::CREATE, $event);

Page 53: How Kris Writes Symfony Apps

$event = new UserUserEvent($dm, $user, $otherUser);!$dispatcher->dispatch(UserEvent::FOLLOW, $event);

Page 54: How Kris Writes Symfony Apps

preFlush

Page 55: How Kris Writes Symfony Apps

public function preFlush(ManagerEventArgs $event)!{! $dm = $event->getObjectManager();! $uow = $dm->getUnitOfWork();!! foreach ($uow->getIdentityMap() as $class => $docs) {! if (is_a($class, 'Kris\Model\User')) {! foreach ($docs as $doc) {! $this->processUserFlush($dm, $doc);! }! } elseif (is_a($class, 'Kris\Model\Widget')) {! foreach ($docs as $doc) {! $this->processWidgetFlush($dm, $doc);! }! }! }!}

Page 56: How Kris Writes Symfony Apps

/** @DI\Observe("user.create") */!public function onUserCreate(UserEvent $event)!{! $user = $event->getUser();!! $activity = new Activity();! $activity->setActor($user);! $activity->setVerb('register');! $activity->setCreatedAt($user->getCreatedAt());!! $this->dm->persist($activity);!}

Page 57: How Kris Writes Symfony Apps

/** @DI\Observe("user.follow_user") */!public function onFollowUser(UserUserEvent $event)!{! $event->getUser()! ->getStats()! ->incrementFollowedUsers(1);! $event->getOtherUser()! ->getStats()! ->incrementFollowers(1);!}

Page 58: How Kris Writes Symfony Apps

Decouple your application by delegating work to clean, concise,

single-purpose event listeners.

Page 59: How Kris Writes Symfony Apps

Contextual Configuration

Page 60: How Kris Writes Symfony Apps

Save your future self a headache

Page 61: How Kris Writes Symfony Apps

# @MainBundle/Resources/config/widget.yml!services:! widget_twiddler:! class: Kris\Bundle\MainBundle\Widget\Twiddler! arguments:! - @event_dispatcher! - @?logger

Page 62: How Kris Writes Symfony Apps

JMSDiExtraBundle

Page 63: How Kris Writes Symfony Apps

/** @DI\Service("widget_twiddler") */!class Twiddler!{! /** @DI\InjectParams */! public function __construct(! EventDispatcherInterface $dispatcher,! LoggerInterface $logger = null)! {! // ...! }!}

Page 64: How Kris Writes Symfony Apps

services:! # aliases for auto-wiring! container: @service_container! dm: @doctrine_mongodb.odm.document_manager! doctrine: @doctrine_mongodb! dispatcher: @event_dispatcher! security: @security.context

Page 65: How Kris Writes Symfony Apps

require.js

Page 66: How Kris Writes Symfony Apps
Page 67: How Kris Writes Symfony Apps

<script src="{{ asset('js/lib/require.js') }}"></script>!<script>!require.config({! baseUrl: "{{ asset('js') }}",! paths: {! "jquery": "//ajax.googleapis.com/.../jquery.min",! "underscore": "lib/underscore",! "backbone": "lib/backbone"! },! shim: {! "jquery": { exports: "jQuery" },! "underscore": { exports: "_" },! "backbone": {! deps: [ "jquery", "underscore" ],! exports: "Backbone"! }! }!})!require([ "main" ])!</script>

Page 68: How Kris Writes Symfony Apps

// web/js/model/user.js!define(! [ "underscore", "backbone" ],! function(_, Backbone) {! var tmpl = _.template("<%- first %> <%- last %>")! return Backbone.Model.extend({! name: function() {! return tmpl({! first: this.get("first_name"),! last: this.get("last_name")! })! }! })! }!)

Page 69: How Kris Writes Symfony Apps

{% block head %}!<script>!require(! [ "view/user", "model/user" ],! function(UserView, User) {! var view = new UserView({! model: new User({{ user|serialize|raw }}),! el: document.getElementById("user")! })! }!)!</script>!{% endblock %}

Page 70: How Kris Writes Symfony Apps

Dependencies

• model: backbone, underscore

• view: backbone, jquery

• template: model, view

Page 71: How Kris Writes Symfony Apps

{% javascripts! "js/lib/jquery.js" "js/lib/underscore.js"! "js/lib/backbone.js" "js/model/user.js"! "js/view/user.js"! filter="?uglifyjs2" output="js/packed/user.js" %}!<script src="{{ asset_url }}"></script>!{% endjavascripts %}!!<script>!var view = new UserView({! model: new User({{ user|serialize|raw }}),! el: document.getElementById("user")!})!</script>

Page 72: How Kris Writes Symfony Apps

Unused dependencies naturally slough off

Page 73: How Kris Writes Symfony Apps

JMSSerializerBundle

Page 74: How Kris Writes Symfony Apps

{% block head %}!<script>!require(! [ "view/user", "model/user" ],! function(UserView, User) {! var view = new UserView({! model: new User({{ user|serialize|raw }}),! el: document.getElementById("user")! })! }!)!</script>!{% endblock %}

Page 75: How Kris Writes Symfony Apps

/** @ExclusionPolicy("ALL") */!class User!{! private $id;!!

/** @Expose */! private $firstName;!!

/** @Expose */! private $lastName;!}

Page 76: How Kris Writes Symfony Apps

Miscellaneous

Page 77: How Kris Writes Symfony Apps

When to create a new bundle

• Anything reusable

• Lots of classes relating to one feature

• Integration with a third party

Page 78: How Kris Writes Symfony Apps

{% include 'MainBundle:Account/Widget:sidebar.html.twig' %}

Page 79: How Kris Writes Symfony Apps

{% include 'AccountBundle:Widget:sidebar.html.twig' %}

Page 80: How Kris Writes Symfony Apps

Access Control

Page 81: How Kris Writes Symfony Apps

The Symfony ACL is for arbitrary permissions

Page 82: How Kris Writes Symfony Apps
Page 83: How Kris Writes Symfony Apps

Encapsulate access logic in custom voter classes

Page 84: How Kris Writes Symfony Apps

/** @DI\Service(public=false) @DI\Tag("security.voter") */!class WidgetVoter implements VoterInterface!{! public function supportsAttribute($attribute)! {! return 'OWNER' === $attribute;! }!! public function supportsClass($class)! {! return is_a($class, 'Kris\Model\Widget');! }!! public function vote(TokenInterface $token, $widget, array $attributes)! {! // ...! }!}

Page 85: How Kris Writes Symfony Apps

public function vote(TokenInterface $token, $widget, array $attributes)!{! $result = VoterInterface::ACCESS_ABSTAIN;! if (!$this->supportsClass(get_class($widget))) {! return $result;! }!! foreach ($attributes as $attribute) {! if (!$this->supportsAttribute($attribute)) {! continue;! }!! $result = VoterInterface::ACCESS_DENIED;! if ($token->getUser() === $widget->getUser()) {! return VoterInterface::ACCESS_GRANTED;! }! }!! return $result;!}

Page 86: How Kris Writes Symfony Apps

JMSSecurityExtraBundle

Page 87: How Kris Writes Symfony Apps

/** @SecureParam(name="widget", permissions="OWNER") */!public function editAction(Widget $widget)!{! // ...!}

Page 88: How Kris Writes Symfony Apps

{% if is_granted('OWNER', widget) %}!{# ... #}!{% endif %}

Page 89: How Kris Writes Symfony Apps

No query builders outside of repositories

Page 90: How Kris Writes Symfony Apps

class WidgetRepository extends DocumentRepository!{! public function findByUser(User $user)! {! return $this->createQueryBuilder()! ->field('userId')->equals($user->getId())! ->getQuery()! ->execute();! }!! public function updateDenormalizedUsernames(User $user)! {! $this->createQueryBuilder()! ->update()! ->multiple()! ->field('userId')->equals($user->getId())! ->field('userName')->set($user->getUsername())! ->getQuery()! ->execute();! }!}

Page 91: How Kris Writes Symfony Apps

Eager ID creation

Page 92: How Kris Writes Symfony Apps

public function __construct()!{! $this->id = (string) new \MongoId();!}

Page 93: How Kris Writes Symfony Apps

public function __construct()!{! $this->id = (string) new \MongoId();! $this->createdAt = new \DateTime();! $this->widgets = new ArrayCollection();!}

Page 94: How Kris Writes Symfony Apps

Remember your clone constructor

Page 95: How Kris Writes Symfony Apps

$foo = new Foo();!$bar = clone $foo;

Page 96: How Kris Writes Symfony Apps

public function __clone()!{! $this->id = (string) new \MongoId();! $this->createdAt = new \DateTime();! $this->widgets = new ArrayCollection(! $this->widgets->toArray()! );!}

Page 97: How Kris Writes Symfony Apps

public function __construct()!{! $this->id = (string) new \MongoId();! $this->createdAt = new \DateTime();! $this->widgets = new ArrayCollection();!}!!public function __clone()!{! $this->id = (string) new \MongoId();! $this->createdAt = new \DateTime();! $this->widgets = new ArrayCollection(! $this->widgets->toArray()! );!}

Page 98: How Kris Writes Symfony Apps

Save space on field names

Page 99: How Kris Writes Symfony Apps

/** @ODM\String(name="u") */!private $username;!!

/** @ODM\String(name="uc") @ODM\UniqueIndex */!private $usernameCanonical;

Page 100: How Kris Writes Symfony Apps

Only flush from the controller

Page 101: How Kris Writes Symfony Apps

public function theAction(Widget $widget)!{! $this->get('widget_twiddler')! ->skeedaddle($widget);! $this->flush();!}

Page 102: How Kris Writes Symfony Apps

No proxy objects

Page 103: How Kris Writes Symfony Apps

/** @ODM\ReferenceOne(targetDocument="User") */!private $user;

Page 104: How Kris Writes Symfony Apps

public function getUser()!{! if ($this->userId && !$this->user) {! throw new UninitializedReferenceException('user');! }!! return $this->user;!}

Page 105: How Kris Writes Symfony Apps

Questions?

Page 106: How Kris Writes Symfony Apps

Thank You!

@kriswallsmith.net

https://joind.in/10371