First, all you need is sylius/order-bundle
version 0.17 (at time of writing)
$ composer require sylius/order-bundle "^0.17"
Then add the necessary bundles to app/AppKernel.php
:
public function registerBundles()
{
$bundles = [
// Bundles you've already registered go here.
// The following bundles are dependencies of Sylius ResourceBundle.
new FOS\RestBundle\FOSRestBundle(),
new JMS\SerializerBundle\JMSSerializerBundle($this),
new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),
// The following Sylius bundles are dependencies of Sylius OrderBundle
new Sylius\Bundle\ResourceBundle\SyliusResourceBundle(),
new Sylius\Bundle\MoneyBundle\SyliusMoneyBundle(),
new Sylius\Bundle\SequenceBundle\SyliusSequenceBundle(),
new Sylius\Bundle\OrderBundle\SyliusOrderBundle(),
// Doctrine bundle MUST be the last bundle registered.
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
];
//....
return $bundles;
}
Now to use the OrderBundle with your own entities you need to create an Order and OrderItem entity that extend the ones provided by Sylius.
For Order
, we'll add a field to capture the visitor's email address. You could capture the current user, or whatever you like to identify who made the order.
<?php
namespace AppBundle\Entity;
use Sylius\Component\Order\Model\Order as SyliusOrder;
class Order extends SyliusOrder
{
/**
* @var string
*/
private $email;
/**
* @return string
*/
public function getEmail()
{
return $this->email;
}
/**
* @param string $email
*/
public function setEmail($email)
{
$this->email = $email;
}
}
For the purpose of this demonstration, we'll assume visitors want to order music tracks for download.
Otherwise, this can be anything. In the Sylius app, it's a Product. In some apps it might not be a product but a subscription or a service, etc.
So for OrderItem
, we'll add a field to capture the downloads the visitor will order.
<?php
namespace AppBundle\Entity;
use Sylius\Component\Order\Model\OrderItem as SyliusOrderItem;
class OrderItem extends SyliusOrderItem
{
/**
* @var Download
*/
private $download;
/**
* @return Download
*/
public function getDownload()
{
return $this->download;
}
/**
* @param Download $download
*/
public function setDownload(Download $download)
{
$this->download = $download;
}
}
Now you have your entities, you can add the Doctrine mapping. In this example, we're using XML (ugly, but it validates the config) but it can be YAML or annotations.
In src/AppBundle/Resources/config/doctrine/Order.orm.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="AppBundle\Entity\Order" table="app_order">
<field name="email" column="email" type="string" nullable="true" />
<one-to-many field="items" target-entity="Sylius\Component\Order\Model\OrderItemInterface" mapped-by="order" orphan-removal="true">
<cascade>
<cascade-all/>
</cascade>
</one-to-many>
<one-to-many field="adjustments" target-entity="Sylius\Component\Order\Model\AdjustmentInterface" mapped-by="order" orphan-removal="true">
<cascade>
<cascade-all/>
</cascade>
</one-to-many>
</entity>
In src/AppBundle/Resources/config/doctrine/OrderItem.orm.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="AppBundle\Entity\OrderItem" table="app_order_item">
<many-to-one field="download" target-entity="AppBundle\Entity\Download">
<join-column name="download_id" referenced-column-name="id" nullable="false" />
</many-to-one>
</entity>
</doctrine-mapping>
Then we need to let the Sylius OrderBundle know about our new entities so that it doesn't use its default ones.
Also we want to generate order numbers for our Order
entity, which is done by the Sylius SequenceBundle.
In app/config/config.yml
, add this configuration:
sylius_sequence:
generators:
AppBundle\Entity\Order: sylius.sequence.sequential_number_generator
sylius_order:
resources:
order:
classes:
model: AppBundle\Entity\Order
order_item:
classes:
model: AppBundle\Entity\OrderItem
Then update your database schema:
$ php app/console doctrine:schema:update --dump-sql --force
Now for order numbers to actually get generated and assigned to the Order
when a new entity is persisted, we need to register a listener.
In app/config/services.yml
OR in your bundle's Resources/config/services.yml
, add this configuration:
parameters:
sylius.model.sequence.class: Sylius\Component\Sequence\Model\Sequence
services:
app.order_number_listener:
class: Sylius\Bundle\OrderBundle\EventListener\OrderNumberListener
arguments:
- "@sylius.sequence.doctrine_number_listener"
tags:
- { name: kernel.event_listener, event: app.download_ordered, method: generateOrderNumber }
The name of the event app.download_ordered
is important here, you can name this anything you wish, but it must be dispatched when you create a new order.
Here is an example of creating a new order, where we dispatch the app.download_ordered
event.
use AppBundle\Entity\Download;
use AppBundle\Entity\OrderItem;
use AppBundle\Form\OrderType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration as Framework;
use AppBundle\Entity\Order;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\EventDispatcher\GenericEvent;
use Symfony\Component\HttpFoundation\Request;
const STATE_BEGIN = 'begin';
const STATE_COMPLETE = 'complete';
/**
* @Framework\Route("/begin-order-for-download/{id}", name="begin_order_for_download")
*/
public function beginOrderForDownloadAction(Request $request, $id)
{
$download = $this->getDoctrine()->getRepository(Download::class)->findOneBy(['id' => $id]);
$order = new Order();
$form = $this->createForm(new OrderType(), $order);
if ('POST' === $request->getMethod()) {
$order->setState(self::STATE_BEGIN);
$orderItem = new OrderItem();
$orderItem->setDownload($download);
$orderItem->setOrder($order);
$orderItem->setUnitPrice(59);
// $orderItem->setImmutable(true); // Need to verify how this affects behavior.
$this->get('event_dispatcher')->dispatch('app.download_ordered', new GenericEvent($order));
$form->handleRequest($request);
$em = $this->getDoctrine()->getManager();
if ($form->isValid()) {
$order->setState(self::STATE_COMPLETE);
$this->addFlash('order.state', self::STATE_COMPLETE);
$em->persist($order);
$em->flush();
return $this->redirectToRoute('complete_order', [
'id' => $order->getId(),
]);
}
}
return $this->render('AppBundle::begin_order_for_download.html.twig', [
'form' => $form->createView(),
'download' => $download,
]);
}
The OrderType
form looks like this, kept simple for demonstration purposes. Edit to your needs.
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class OrderType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function getName()
{
return 'order';
}
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('email', 'repeated', [
'type' => 'email',
'first_options' => ['label' => 'Email'],
'second_options' => ['label' => 'Repeat Email'],
'invalid_message' => 'The email fields must match.',
]);
$builder->add('proceed', 'submit', ['attr' => ['class' => 'button']]);
}
}
If the form was valid, our order would be persisted and the listener assign it an order number, then we would be rediect complete_order
route. Here you'd do things like provide payment detials or in this case, provide a link to download the song.
That's it. You can see the code for the full working example at https://github.com/adamelso/orda