How Kris Writes Symfony Apps

Post on 08-Sep-2014

19 views 4 download

Tags:

description

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.

Transcript of How Kris Writes Symfony Apps

How Kris Writes Symfony Apps

@kriswallsmith

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…

assetic

Buzz

Spork

Getting Started

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

- "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",

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

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

Model

Treat your model like a princess.

She gets her own wing of the palace…

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

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

…doesn't do any work…

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

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

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

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

Anemic domain model is an anti-pattern?

Anemic???

“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

$cabinet->open();

Cabinets don’t open themselves.

$asset->getLastModified();

Mapping Layers

thin

thin controller fat model

MVC

Is Symfony an MVC framework?

HTTP

HT

TP Land

Application Land

Controller

The controller is thin because it maps from

HTTP-land to application-land.

What about the model?

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

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

Application Land

Persistence Land

Model

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

Model

Application Land

Persistence Land

HT

TP Land

Controller

Who lives in application land?

Thin controller, thin model… Fat service layer!

Application Events

Use them.

That happened.

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

One event class per model

• Event name constants

• Accepts object manager and object as arguments

• Simple accessors

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

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

preFlush

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

/** @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);!}

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

Decouple your application by delegating work to clean, concise,

single-purpose event listeners.

Contextual Configuration

Save your future self a headache

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

JMSDiExtraBundle

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

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

require.js

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

// 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")! })! }! })! }!)

{% 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 %}

Dependencies

• model: backbone, underscore

• view: backbone, jquery

• template: model, view

{% 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>

Unused dependencies naturally slough off

JMSSerializerBundle

{% 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 %}

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

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

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

Miscellaneous

When to create a new bundle

• Anything reusable

• Lots of classes relating to one feature

• Integration with a third party

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

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

Access Control

The Symfony ACL is for arbitrary permissions

Encapsulate access logic in custom voter classes

/** @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)! {! // ...! }!}

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

JMSSecurityExtraBundle

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

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

No query builders outside of repositories

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

Eager ID creation

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

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

Remember your clone constructor

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

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

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

Save space on field names

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

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

Only flush from the controller

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

No proxy objects

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

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

Questions?

Thank You!

@kriswallsmith.net

https://joind.in/10371