How Kris Writes Symfony Apps
Mapping Layers
thin
thin controller fat model
MVC
Is Symfony an MVC framework?
HTTP
Symfony is an HTTP framework
Application Land 
Controller 
HTTP Land
The controller is thin 
because it maps from 
HTTP-land to application-land.
What about the model?
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
Should there be managers?
Application Events
Listen
/** @DIObserve(UserEvent::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(UserEvent::USERNAME_CHANGE) */ 
public function onUsernameChange(UserEvent $event) 
{ 
$user = $event->getUser(); 
$dm = $event->getDocumentManager(); 
$dm->getRepository('Model:Widget') 
->updateDenormalizedUsernames($user); 
}
/** @DIObserve(UserEvent::FOLLOW) */ 
public function onFollow(UserUserEvent $event) 
{ 
$event->getUser() 
->getStats() 
->incrementFollowedUsers(1); 
$event->getOtherUser() 
->getStats() 
->incrementFollowers(1); 
}
Dispatch
$event = new UserEvent($dm, $user); 
$dispatcher->dispatch(UserEvent::CREATE, $event);
$event = new UserEvent($dm, $user); 
$dispatcher->dispatch(UserEvent::UPDATE, $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, 'KrisModelUser')) { 
foreach ($docs as $doc) { 
$this->processUserFlush($dm, $doc); 
} 
} elseif (is_a($class, 'KrisModelWidget')) { 
foreach ($docs as $doc) { 
$this->processWidgetFlush($dm, $doc); 
} 
} 
} 
}
Decouple your application by 
delegating work to clean, concise, 
single-purpose event listeners.
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: KrisModel 
alias: Model
// repo for src/Kris/Model/User.php 
$repo = $this->dm->getRepository('Model:User');
…doesn't do any work…
use KrisBundleMainBundleCanonicalizer; 
public function setUsername($username) 
{ 
$this->username = $username; 
$canonicalizer = Canonicalizer::instance(); 
$this->usernameCanonical = $canonicalizer->canonicalize($username); 
}
use KrisBundleMainBundleCanonicalizer; 
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; 
}
Cabinets don’t open themselves.
Contextual Configuration
Save your future self a headache.
# @MainBundle/Resources/config/widget.yml 
services: 
widget_twiddler: 
class: KrisBundleMainBundleWidgetTwiddler 
arguments: 
- @event_dispatcher 
- @?logger
JMSDiExtraBundle
/** @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
require.js
{% 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 %}
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; 
}
Five more things…
When to create a new bundle 
• Anything reusable 
• A new feature 
• 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
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() 
); 
}
Only flush from the controller
public function theAction(Widget $widget) 
{ 
$this->get('widget_twiddler') 
->skeedaddle($widget); 
$this->flush(); 
}
Questions?

How kris-writes-symfony-apps-london