Upload
kris-wallsmith
View
973
Download
0
Embed Size (px)
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.
Citation preview
How Kris Writes Symfony Apps
Mapping Layers
thin
thin controller fat model
MVC
Is Symfony an MVC framework?
HTTP
Symfony is an HTTP framework
HT
TP Land
Application Land
Controller
The controller is thin because it maps from
HTTP-land to application-land.
What about the model?
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
Should there be managers?
Application Events
Listen
/** @DI\Observe(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);}
/** @DI\Observe(UserEvent::USERNAME_CHANGE) */public function onUsernameChange(UserEvent $event){ $user = $event->getUser(); $dm = $event->getDocumentManager();
$dm->getRepository('Model:Widget') ->updateDenormalizedUsernames($user);}
/** @DI\Observe(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, '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); } } }}
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: Kris\Model alias: Model
// repo for src/Kris/Model/User.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;}
Cabinets don’t open themselves.
Contextual Configuration
Save your future self a headache.
# @MainBundle/Resources/config/widget.ymlservices: 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
{% 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?