How Kris Writes Symfony Apps
       @kriswallsmith • February 9, 2013
About Me
@kriswallsmith.net

•   Born, raised, & live in Portland

•   10+ years of experience

•   Lead Architect at OpenSky

•   Open source fanboy
brewcycleportland.com
assetic
Buzz
Spork
Go big or go home.
Getting Started
composer create-project 
    symfony/framework-standard-edition 
    opti-grab/ 2.2.x-dev
-   "doctrine/orm": "~2.2,>=2.2.3",
-   "doctrine/doctrine-bundle": "1.2.*",
+   "doctrine/mongodb-odm-bundle": "3.0.*",
+   "jms/serializer-bundle": "1.0.*",
./app/console generate:bundle 
    --namespace=OptiGrab/Bundle/MainBundle
assetic:
    debug:             %kernel.debug%
    use_controller:    false
    bundles:           [ MainBundle ]
    filters:
         cssrewrite:   ~
         uglifyjs2:    { compress: true, mangle: true }
         uglifycss:    ~
jms_di_extra:
    locations:
        bundles:
            - 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);
    }
}
MongoDB
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:     optiGrab
            mappings:
                 model:
                     type:    annotation
                     dir:     %src_dir%/OptiGrab/Model
                     prefix: OptiGrabModel
                     alias: Model
// repo for src/OptiGrab/Model/Widget.php
$repo = $this->dm->getRepository('Model:User');
…doesn't do any work…
use OptiGrabBundleMainBundleCanonicalizer;

public function setUsername($username)
{
    $this->username = $username;

    $canonicalizer = Canonicalizer::instance();
    $this->usernameCanonical = $canonicalizer->canonicalize($username);
}
use OptiGrabBundleMainBundleCanonicalizer;

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;
}
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()
    );
}
Only flush from the controller
public function theAction(Widget $widget)
{
    $this->get('widget_twiddler')
         ->skeedaddle($widget);
    $this->flush();
}
Save space on field names
/** @ODMString(name="u") */
private $username;

/** @ODMString(name="uc") @ODMUniqueIndex */
private $usernameCanonical;
public function getUsername()
{
    return $this->username ?: $this->usernameCanonical;
}

public function setUsername($username)
{
    if ($username) {
        $this->usernameCanonical = strtolower($username);
        $this->username = $username === $this->usernameCanonical ? null : $username;
    } else {
        $this->usernameCanonical = null;
        $this->username = null;
    }
}
No proxy objects
/** @ODMReferenceOne(targetDocument="User") */
private $user;
public function getUser()
{
    if ($this->userId && !$this->user) {
        throw new UninitializedReferenceException('user');
    }

    return $this->user;
}
Mapping Layers
What is a mapping layer?
A mapping layer is thin
Thin controller, fat model…
Is Symfony an MVC framework?
Symfony is an HTTP framework
Application Land
  Controller
  HTTP Land
The controller maps from
HTTP-land to application-land.
What about the model?
public function registerAction()
{
    // ...
    $user->sendWelcomeEmail();
    // ...
}
public function registerAction()
{
    // ...
    $mailer->sendWelcomeEmail($user);
    // ...
}
Persistence Land
    Model
Application Land
The model maps from
application-land to persistence-land.
Persistence Land
     Model
Application Land
  Controller
  HTTP Land
Who lives in application land?
Thin controller, thin model…
      Fat service layer!
Application Events
Use lots of them
That happened.
/** @DIObserve("user.username_change") */
public function onUsernameChange(UserEvent $event)
{
    $user = $event->getUser();
    $dm   = $event->getDocumentManager();

    $dm->getRepository('Model:Widget')
       ->updateDenormalizedUsernames($user);
}
Unit of Work
public function onFlush(OnFlushEventArgs $event)
{
    $dm = $event->getDocumentManager();
    $uow = $dm->getUnitOfWork();

    foreach ($uow->getIdentityMap() as $class => $docs) {
        if (self::checkClass('OptiGrabModelUser', $class)) {
            foreach ($docs as $doc) {
                $this->processUserFlush($dm, $doc);
            }
        } elseif (self::checkClass('OptiGrabModelWidget', $class)) {
            foreach ($docs as $doc) {
                $this->processWidgetFlush($dm, $doc);
            }
        }
    }
}
private function processUserFlush(DocumentManager $dm, User $user)
{
    $uow     = $dm->getUnitOfWork();
    $meta    = $dm->getClassMetadata('Model:User');
    $changes = $uow->getDocumentChangeSet($user);

    if (isset($changes['id'][1])) {
        $this->dispatcher->dispatch(UserEvents::CREATE, new UserEvent($dm, $user));
    }

    if (isset($changes['usernameCanonical'][0]) && null !== $changes['usernameCanonical'][0]) {
        $this->dispatcher->dispatch(UserEvents::USERNAME_CHANGE, new UserEvent($dm, $user));
    }

    if ($followedUsers = $meta->getFieldValue($user, 'followedUsers')) {
        foreach ($followedUsers->getInsertDiff() as $otherUser) {
            $this->dispatcher->dispatch(
                UserEvents::FOLLOW_USER,
                new UserUserEvent($dm, $user, $otherUser)
            );
        }

        foreach ($followedUsers->getDeleteDiff() as $otherUser) {
            // ...
        }
    }
}
/** @DIObserve("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);
}
/** @DIObserve("user.create") */
public function onUserCreate(UserEvent $event)
{
    $dm   = $event->getDocumentManager();
    $user = $event->getUser();

    $widget = new Widget();
    $widget->setUser($user);

    $dm->persist($widget);

    // manually notify the event
    $event->getDispatcher()->dispatch(
        WidgetEvents::CREATE,
        new WidgetEvent($dm, $widget)
    );
}
/** @DIObserve("user.follow_user") */
public function onFollowUser(UserUserEvent $event)
{
    $event->getUser()
          ->getStats()
          ->incrementFollowedUsers(1);
    $event->getOtherUser()
          ->getStats()
          ->incrementFollowers(1);
}
Two event classes per model
• @MainBundleUserEvents: encapsulates event name constants
  such as UserEvents::CREATE and
  UserEvents::CHANGE_USERNAME

• @MainBundleEventUserEvent: base event object, accepts
  $dm and $user arguments

• @MainBundleWidgetEvents…
• @MainBundleEventWidgetEvent…
$event = new UserEvent($dm, $user);
$dispatcher->dispatch(UserEvents::CREATE, $event);
Delegate 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: OptiGrabBundleMainBundleWidgetTwiddler
        arguments:
            - @event_dispatcher
            - @?logger
/** @DIService("widget_twiddler") */
class Twiddler
{
    /** @DIInjectParams */
    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
JMSDiExtraBundle
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
Lots of classes pertaining to
        one feature
{% 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
/** @DIService(public=false) @DITag("security.voter") */
class WidgetVoter implements VoterInterface
{
    public function supportsAttribute($attribute)
    {
        return 'OWNER' === $attribute;
    }

    public function supportsClass($class)
    {
        return 'OptiGrabModelWidget' === $class
            || is_subclass_of($class, 'OptiGrabModelWidget');
    }

    public function vote(TokenInterface $token, $widget, array $attributes)
    {
        // ...
    }
}
public function vote(TokenInterface $token, $map, array $attributes)
{
    $result = VoterInterface::ACCESS_ABSTAIN;

    if (!$this->supportsClass(get_class($map))) {
        return $result;
    }

    foreach ($attributes as $attribute) {
        if (!$this->supportsAttribute($attribute)) {
            continue;
        }

        $result = VoterInterface::ACCESS_DENIED;
        if ($token->getUser() === $map->getUser()) {
            return VoterInterface::ACCESS_GRANTED;
        }
    }

    return $result;
}
/** @SecureParam(name="widget", permissions="OWNER") */
public function editAction(Widget $widget)
{
    // ...
}
{% if is_granted('OWNER', widget) %}
{# ... #}
{% endif %}
Only mock interfaces
interface FacebookInterface
{
    function getUser();
    function api();
}

/** @DIService("facebook") */
class Facebook extends BaseFacebook implements FacebookInterface
{
    // ...
}
$facebook = $this->getMock('OptiGrabBundleMainBundleFacebookFacebookInterface');
$facebook->expects($this->any())
    ->method('getUser')
    ->will($this->returnValue(123));
Questions?
@kriswallsmith.net



        joind.in/8024


      Thank You!

How Kris Writes Symfony Apps