Hautelook is a large ecommerce application that is currently running a Zend Framework 1 backend. The next iteration of its API (used by desktop, mobile, as well as iPhone and Android native applications) is done with Symfony 2. This API is following the principles for hypermedia APIs. To that end, Hal+Json is the media-type we chose, and we implemented most of it using the FSC HateoasBundle. Another critical piece of Hal+Json APIs is documentation. To this end we have used NelmioApiDocBundle to automatically generate documentation for the API endpoints. The other critical piece of any application is performance for which we use XHProf with XHGui. In my talk I want to touch on all those aspects, show some of the lessons learned, how we solved some of the problems, and what is still unsolved.
3. About me
architecting at Hautelook
contributing on GitHub: @baldurrensch
opinionating on twitter: @brensch
4. What is ?
Member only
shopping site
Private, limited-
time sale events
daily email
invitation at 8am
5. Some stats
Alexa traffic rank for US: 847
More than 12 million members (on average 20k new
members per day)
Up to 200 orders per minute
Massive traffic spikes (remember, 8am)
[1]
6. [1]
Some stats
Alexa traffic rank for US: 847
More than 12 million members (on average 20k new
members per day)
Up to 200 orders per minute
Massive traffic spikes (remember, 8am) daily
21. Issues
This is fine when you have 5 end points and simple
responses.
Lots of boiler plate code
Zend Framework 1 did not scale very well. We
constantly had to overwrite parts of the framework.
22. Moving from Imperative to Declarative
Programming
[4,5]
Imperative Declarative
“In computer science,
imperative programming is a
programming paradigm that
describes computation in
terms of statements that
change a program state.”
“In computer science,
declarative programming is a
programming paradigm that
expresses the logic of a
computation without
describing its control flow.”
23. [4,5]
Imperative Declarative
“In computer science,
imperative programming is a
programming paradigm that
describes computation in
terms of statements that
change a program state.”
“In computer science,
declarative programming is a
programming paradigm that
expresses the logic of a
computation without
describing its control flow.”
Moving from Imperative to Declarative
Programming
25. Advantages:
Symfony allows for way more declarative programming
which allows us to write less code.
Allows us to extend way easier. And it’s actually fun.
Community is great.
26. Bundles we use
Friends of Symphony: RestBundle
Nelmio: ApiDocBundle, SolariumBundle
JMS: SerializerBundle
Football Social Club: HateoasBundle
Hautelook: GearmanBundle
27. /**
* This function returns a member's cart
*
* @Route("/members/{memberId}/cart", requirements = { "memberId" = "d+" } )
* @Method({"GET"})
*
* @ApiDoc(
* resource=true,
* description="Retrieve a member's cart",
* statusCodes={
* 200="Returned when successful",
* 400="Returned when the request is not well-formed",
* 403="Returned when the user is not authorized or not owner or have no admin access",
* 404="Returned when the member is not found"
* }
* )
*
* @param int $memberId
*
* @return Response
*/
public function getCartAction($memberId)
{
$member = $this->get('hautelook.model.member')->getMemberAndValidate($memberId);
$cart = $this->get('hautelook.model.cart')->getCart($member);
$response = $this->get('serializer')->serialize($cart, 'json');
$response = new Response($response);
$response->headers->set('Content-Type', 'application/json');
$response->setETag(md5($response->getContent()));
return $response;
}
Controller
Service
Model/
View
28. Controller
Service
Model/
View
Routing, Input
Validation
/**
* This function returns a member's cart
*
* @Route("/members/{memberId}/cart", requirements = { "memberId" = "d+" } )
* @Method({"GET"})
*
* @ApiDoc(
* resource=true,
* description="Retrieve a member's cart",
* statusCodes={
* 200="Returned when successful",
* 400="Returned when the request is not well-formed",
* 403="Returned when the user is not authorized or not owner or have no admin access",
* 404="Returned when the member is not found"
* }
* )
*
* @param int $memberId
*
* @return Response
*/
public function getCartAction($memberId)
{
$member = $this->get('hautelook.model.member')->getMemberAndValidate($memberId);
$cart = $this->get('hautelook.model.cart')->getCart($member);
$response = $this->get('serializer')->serialize($cart, 'json');
$response = new Response($response);
$response->headers->set('Content-Type', 'application/hal+json');
$response->setETag(md5($response->getContent()));
return $response;
}
29. Controller
Service
Model/
View
Documentation
/**
* This function returns a member's cart
*
* @Route("/members/{memberId}/cart", requirements = { "memberId" = "d+" } )
* @Method({"GET"})
*
* @ApiDoc(
* resource=true,
* description="Retrieve a member's cart",
* statusCodes={
* 200="Returned when successful",
* 400="Returned when the request is not well-formed",
* 403="Returned when the user is not authorized or not owner or have no admin access",
* 404="Returned when the member is not found"
* }
* )
*
* @param int $memberId
*
* @return Response
*/
public function getCartAction($memberId)
{
$member = $this->get('hautelook.model.member')->getMemberAndValidate($memberId);
$cart = $this->get('hautelook.model.cart')->getCart($member);
$response = $this->get('serializer')->serialize($cart, 'json');
$response = new Response($response);
$response->headers->set('Content-Type', 'application/hal+json');
$response->setETag(md5($response->getContent()));
return $response;
}
30. Controller
Service
Model/
View
Call Service to get data
/**
* This function returns a member's cart
*
* @Route("/members/{memberId}/cart", requirements = { "memberId" = "d+" } )
* @Method({"GET"})
*
* @ApiDoc(
* resource=true,
* description="Retrieve a member's cart",
* statusCodes={
* 200="Returned when successful",
* 400="Returned when the request is not well-formed",
* 403="Returned when the user is not authorized or not owner or have no admin access",
* 404="Returned when the member is not found"
* }
* )
*
* @param int $memberId
*
* @return Response
*/
public function getCartAction($memberId)
{
$member = $this->get('hautelook.model.member')->getMemberAndValidate($memberId);
$cart = $this->get('hautelook.model.cart')->getCart($member);
$response = $this->get('serializer')->serialize($cart, 'json');
$response = new Response($response);
$response->headers->set('Content-Type', 'application/hal+json');
$response->setETag(md5($response->getContent()));
return $response;
}
31. Controller
Service
Model/
View
Create
response
/**
* This function returns a member's cart
*
* @Route("/members/{memberId}/cart", requirements = { "memberId" = "d+" } )
* @Method({"GET"})
*
* @ApiDoc(
* resource=true,
* description="Retrieve a member's cart",
* statusCodes={
* 200="Returned when successful",
* 400="Returned when the request is not well-formed",
* 403="Returned when the user is not authorized or not owner or have no admin access",
* 404="Returned when the member is not found"
* }
* )
*
* @param int $memberId
*
* @return Response
*/
public function getCartAction($memberId)
{
$member = $this->get('hautelook.model.member')->getMemberAndValidate($memberId);
$cart = $this->get('hautelook.model.cart')->getCart($member);
$response = $this->get('serializer')->serialize($cart, 'json');
$response = new Response($response);
$response->headers->set('Content-Type', 'application/hal+json');
$response->setETag(md5($response->getContent()));
return $response;
}
32. public function getCart(Members $member)
{
$cartItems = $this->doctrine->getRepository("HautelookApiBundle:CartItems")
->getCartItems($member->getMemberId());
$cartItemArray = array();
foreach ($cartItems as $cartItem) {
$cartItemArray []= new CartItem($cartItem);
}
$cart = new CartModel($member, $cartItemArray);
return $cart;
}
Controller
Service
Model/
View
33. Controller
Service
Model/
View
Get entities from database
public function getCart(Members $member)
{
$cartItems = $this->doctrine->getRepository("HautelookApiBundle:CartItems")
->getCartItems($member->getMemberId());
$cartItemArray = array();
foreach ($cartItems as $cartItem) {
$cartItemArray []= new CartItem($cartItem);
}
$cart = new CartModel($member, $cartItemArray);
return $cart;
}
34. Controller
Service
Model/
ViewConvert to view
model
Get entities from database
public function getCart(Members $member)
{
$cartItems = $this->doctrine->getRepository("HautelookApiBundle:CartItems")
->getCartItems($member->getMemberId());
$cartItemArray = array();
foreach ($cartItems as $cartItem) {
$cartItemArray []= new CartItem($cartItem);
}
$cart = new CartModel($member, $cartItemArray);
return $cart;
}
38. URI Templates are sexy
There is a RFC for it: RFC-6570
[6, 7, 12]
/demo?{&page}{&sort%5B%5D*}{&filter%5B%5D*}
1 And are the magic that make Hateoas possible
1
39. URI Templates are sexy
/demo?{&page}{&sort%5B%5D*}{&filter%5B%5D*}
There is a RFC for it: RFC-6570
There is a bundle for it™: TemplatedURIBundle
[6, 7, 12]1 And are the magic that make Hateoas possible
1
40. URI Templates are sexy
/demo?{&page}{&sort%5B%5D*}{&filter%5B%5D*}
$templateLink = $this->get('hautelook.router.template')->generate('hautelook_demo_route',
array(
'page' => '{page}',
'sort' => array('{sort}'),
'filter' => array('{filter}'),
)
);
There is a RFC for it: RFC-6570
There is a bundle for it™: TemplatedURIBundle
[6, 7, 12]1 And are the magic that make Hateoas possible
1
41. URI Templates are sexy
/demo?{&page}{&sort%5B%5D*}{&filter%5B%5D*}
$templateLink = $this->get('hautelook.router.template')->generate('hautelook_demo_route',
array(
'page' => '{page}',
'sort' => array('{sort}'),
'filter' => array('{filter}'),
)
);
There is a RFC for it: RFC-6570
There is a bundle for it™: TemplatedURIBundle
It even integrates with the HateoasBundle:
[6, 7, 12]1 And are the magic that make Hateoas possible
1
hautelook_style_image_resizable:
pattern: /resizer/{width}x{height}/products/{styleNum}/{size}/{imageId}.jpg
defaults:
width: "{width}"
height: "{height}"
42. URI Templates are sexy
/demo?{&page}{&sort%5B%5D*}{&filter%5B%5D*}
/**
* @RestRelation("http://hautelook.com/rels/image/resizable",
* href = @RestRoute("hautelook_style_image_resizable",
* parameters = { "styleNum": ".solrDocument.styleNum", "imageId": ".firstImageId" },
* options = { "router": "templated" }
* ),
* excludeIf = { ".firstImageId": null },
* attributes = { "templated": true }
* )
*/
$templateLink = $this->get('hautelook.router.template')->generate('hautelook_demo_route',
array(
'page' => '{page}',
'sort' => array('{sort}'),
'filter' => array('{filter}'),
)
);
There is a RFC for it: RFC-6570
There is a bundle for it™: TemplatedURIBundle
It even integrates with the HateoasBundle:
[6, 7, 12]1 And are the magic that make Hateoas possible
1
50. Measuring performance with declarative
programming
Why its difficult
XHProf to the rescue
Hierarchical function-level profiler
[8, 9, 10]
51. Measuring performance with declarative
programming
Why its difficult
XHProf to the rescue
Hierarchical function-level profiler
PHP Extension
[8, 9, 10]
52. Measuring performance with declarative
programming
Why its difficult
XHProf to the rescue
Hierarchical function-level profiler
PHP Extension
Written by Facebook
[8, 9, 10]
53. Measuring performance with declarative
programming
Why its difficult
XHProf to the rescue
Hierarchical function-level profiler
PHP Extension
Written by Facebook
XHGui on top of XHProf
[8, 9, 10]
54. Measuring performance with declarative
programming
Why its difficult
XHProf to the rescue
Hierarchical function-level profiler
PHP Extension
Written by Facebook
XHGui on top of XHProf
Uses a shared database backend
[8, 9, 10]
55. Measuring performance with declarative
programming
Why its difficult
XHProf to the rescue
Hierarchical function-level profiler
PHP Extension
Written by Facebook
XHGui on top of XHProf
Uses a shared database backend
There is a bundle for that™ as well!
[8, 9, 10]
62. Lessons learned
Large scale Symfony deployments are not that
common
A lot of modules that larger applications need don’t
exist
63. Lessons learned
Large scale Symfony deployments are not that
common
A lot of modules that larger applications need don’t
exist
Example: Session storage in multiple storage layers
such as: Memcached and Database
64. Lessons learned
Large scale Symfony deployments are not that
common
A lot of modules that larger applications need don’t
exist
Example: Session storage in multiple storage layers
such as: Memcached and Database
There is a bundle for that™ now as well:
SessionStorageHandlerChainBundle
[11]
65. Lessons learned
Large scale Symfony deployments are not that
common
A lot of modules that larger applications need don’t
exist
Need more documentation / community around
enterprise level Symfony development
66. Lessons learned
Large scale Symfony deployments are not that
common
A lot of modules that larger applications need don’t
exist
Need more documentation / community around
enterprise level Symfony development
Our Developers love developing in Symfony
67. Questions?
Let’s get in touch for feedback, questions, discussion
Leave feedback at: https://joind.in/8676
Connect on Twitter or Github.