Post on 08-Sep-2014
description
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