Sylius : Wrong total order amount passed to Stripe - symfony

I added a new processor allowing to calculate the amount of the gift card.
services:
app.order_processing.gift_card_processor:
class: App\OrderProcessing\GiftCardProcessor
arguments:
- '#sylius.factory.adjustment'
- '#translator'
tags:
- { name: sylius.order_processor, priority: 5 }
<?php
declare(strict_types=1);
namespace App\OrderProcessing;
use App\Entity\Order\Adjustment;
use App\Entity\Order\Order;
use Sylius\Component\Order\Model\OrderInterface as BaseOrderInterface;
use Sylius\Component\Order\Processor\OrderProcessorInterface;
use Sylius\Component\Resource\Factory\FactoryInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Webmozart\Assert\Assert;
final class GiftCardProcessor implements OrderProcessorInterface
{
private FactoryInterface $adjustmentFactory;
private TranslatorInterface $translator;
public function __construct(
FactoryInterface $adjustmentFactory,
TranslatorInterface $translator
) {
$this->adjustmentFactory = $adjustmentFactory;
$this->translator = $translator;
}
public function process(BaseOrderInterface $order): void
{
/** #var Order $order */
Assert::isInstanceOf($order, Order::class);
// Remove all gift card adjustments, we recalculate everything from scratch.
$order->removeAdjustments(Adjustment::ORDER_GIFT_CARD_ADJUSTMENT);
foreach ($order->getGiftCardOrders() as $giftCardOrder) {
$giftCard = $giftCardOrder->getGiftCard();
$giftCardRemainingAmount = (int) $giftCard->getRemainingAmount() * 100;
$amount = $giftCardRemainingAmount > $order->getTotal() ? $order->getTotal() : $giftCardRemainingAmount;
/** #var Adjustment $adjustment */
$adjustment = $this->adjustmentFactory->createNew();
$adjustment->setType(Adjustment::ORDER_GIFT_CARD_ADJUSTMENT);
$adjustment->setAmount(-$amount);
$adjustment->setLabel($this->translator->trans('gift_card.ui.gift_card'));
$giftCard->addAdjustment($adjustment);
$giftCardOrder->setAmount($amount / 100);
$order->addAdjustment($adjustment);
}
}
}
The total order amount displayed in the cart and inserted in the database is correct (screenshot 1 and 2).
SCREENSHOT 1
SCREENSHOT 2
The bug occurs during payment on stripe, the amount displayed corresponds to the initial amount which does not support the reduction of the gift card (screenshot 3)
SCREENSHOT 3

If you are using this Sylius plugin : flux-se/sylius-payum-stripe-plugin you have to create individual coupons representing your gift cards decorating : https://github.com/FLUX-SE/SyliusPayumStripePlugin/blob/master/src/Provider/DetailsProvider.php the new array member to create is the discounts like the Stripe doc is defining it : https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-discounts
The plugin is only taking care of default Sylius adjustments linked to an OrderItem or an OrderItemUnit. If the adjustment is linked on the Order then it won't be taken into account because Stripe is only making a sum of all line_item as total. Stripe is not allowing negative amount for a line item, that's why coupons are the only way to reduce the total amount of the payment.
Here is the required payum extension handling the creation of coupons if you label the coupon ids with this format sprintf('GIFT_CARD_%s', $giftCard->getCode()) :
<?php
declare(strict_types=1);
namespace App\GiftCard\Payum\Extension;
use FluxSE\PayumStripe\Request\Api\Resource\CreateCoupon;
use FluxSE\PayumStripe\Request\Api\Resource\RetrieveCoupon;
use FluxSE\SyliusPayumStripePlugin\Action\ConvertPaymentAction;
use Payum\Core\Extension\Context;
use Payum\Core\Extension\ExtensionInterface;
use Payum\Core\Request\Convert;
use Stripe\Exception\ApiErrorException;
use Sylius\Component\Core\Model\PaymentInterface;
final class CheckCouponsExtension implements ExtensionInterface
{
public function onPreExecute(Context $context)
{
}
public function onExecute(Context $context)
{
}
public function onPostExecute(Context $context)
{
if ($context->getException()) {
return;
}
if (false === $context->getAction() instanceof ConvertPaymentAction) {
return;
}
/** #var mixed|Convert $request */
$request = $context->getRequest();
if (false === $request instanceof Convert) {
return;
}
/** #var mixed|PaymentInterface $payment */
$payment = $request->getSource();
if (false === $payment instanceof PaymentInterface) {
return;
}
$order = $payment->getOrder();
if (null === $order) {
return;
}
$gateway = $context->getGateway();
foreach ($order->getGiftCardOrders() as $giftCardOrder) {
$giftCard = $giftCardOrder->getGiftCard();
$couponId = sprintf('GIFT_CARD_%s', $giftCard->getCode());
$retrieveCouponRequest = new RetrieveCoupon($couponId);
try {
$gateway->execute($retrieveCouponRequest);
} catch (ApiErrorException $e) {
$createCouponRequest = new CreateCoupon([
'id' => $couponId,
"amount_off" => $giftCard->getAmount()/100,
"currency" => $order->getCurrencyCode(),
"metadata" => [
'SYLIUS_GIFTCARD_ID' => $giftCard->getId(),
'SYLIUS_GIFTCARD_CODE' => $giftCard->getCode(),
],
"name" => sprintf("Gift card #%d", $giftCard->getId()),
]);
$gateway->execute($createCouponRequest);
}
}
}
}
And here is the service declaration :
services:
App\GiftCard\Payum\Extension\CheckCouponsExtension:
public: true
tags:
- name: payum.extension
alias: app.extension.check_coupons
factory: stripe_checkout_session

Related

How to disable a Doctrine subscriber in a Symfony command?

I would like to know if it is possible to disable the subscriber inside a command (only during this command) with Symfony.
I need to update a database but, each time an entity is updated, it will trigger the subscriber and take to much time.
The subscriber is useful for a small amount of modification, but not when the entire database is updated.
I use prePersist, preUpdate, postUpdate and postPersist. There is some example :
<?php
namespace App\EventSubscriber\ElasticSearch;
use App\Entity\Product;
use App\Entity\ProductTranslation;
use App\Service\ElasticSearch\ElasticSearchProductService;
use Cocur\Slugify\Slugify;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
use Doctrine\Persistence\Event\LifecycleEventArgs;
class ProductSubscriber implements EventSubscriber
{
private ElasticSearchProductService $elasticSearchProductService;
public function __construct(ElasticSearchProductService $elasticSearchProductService)
{
$this->elasticSearchProductService = $elasticSearchProductService;
}
/**
* #return array
*/
public function getSubscribedEvents(): array
{
return [
Events::prePersist,
Events::preUpdate,
Events::postPersist,
Events::postUpdate,
];
}
/**
* #param LifecycleEventArgs $args
*/
public function prePersist(LifecycleEventArgs $args): void
{
$product = $args->getObject();
if (!$product instanceof Product) {
return;
}
$this->slugProduct($product);
}
/**
* #param LifecycleEventArgs $args
*/
public function preUpdate(LifecycleEventArgs $args): void
{
$product = $args->getObject();
if (!$product instanceof Product) {
return;
}
$this->slugProduct($product);
}
/**
* #param LifecycleEventArgs $args
*/
public function postUpdate(LifecycleEventArgs $args): void
{
$this->initializeElasticSearch($args);
}
/**
* #param LifecycleEventArgs $args
*/
public function postPersist(LifecycleEventArgs $args): void
{
$this->initializeElasticSearch($args);
}
/**
* #param Product $product
*/
private function slugProduct(Product $product): void
{
$product->setSlug((new Slugify())->slugify($product->getTranslations()['fr']->getName() . '-' . $product->getEan() . '-' . $product->getPickRef()));
}
/**
* #param LifecycleEventArgs $args
*/
private function initializeElasticSearch(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
if (!($entity instanceof Product || $entity instanceof ProductTranslation)) {
return;
}
if ($entity instanceof ProductTranslation) {
$entity = $args->getObject()->getTranslatable();
}
//$this->elasticSearchProductService->initializeDocumentElasticSearch($entity);
}
}
EDIT : I have updated the example. This is the full subscriber. I update a Brand with for example :
$this->em->persist($brand);
$this->em->flush();
It will activate the subscriber who will update ElasticSearch. But, in one Symfony Command that I made,I have to update 40 000 brands. I would like to update elasticSearch at the end of the command and not update one by one with the subscriber. So I would like to disable (for only this Command) the subscriber.
This is a Doctrine EventSubscriber and I tried
$listeners = $this->em->getEventManager()->getListeners("productSubscriber");
foreach ($listeners as $key => $listener) {
if ($listener instanceof ProductSubscriber) {
$this->em->getEventManager()->removeEventListener($listener->getSubscribedEvents(), $listener);
}
}
But the subscriber is not removed.

How to hide item from collection depending on some field value?

I override (custom operation and service) the DELETE operation of my app to avoid deleting data from DB. What I do is I update a field value: isDeleted === true.
Here is my controller :
class ConferenceDeleteAction extends BaseAction
{
public function __invoke(EntityService $entityService, Conference $data)
{
$entityService->markAsDeleted($data, Conference::class);
}
...
My service :
class EntityService extends BaseService
{
public function markAsDeleted(ApiBaseEntity $data, string $className)
{
/**
* #var ApiBaseEntity $entity
*/
$entity = $this->em->getRepository($className)
->findOneBy(["id" => $data->getId()]);
if ($entity === null || $entity->getDeleted()) {
throw new NotFoundHttpException('Unable to find this resource.');
}
$entity->setDeleted(true);
if ($this->dataPersister->supports($entity)) {
$this->dataPersister->persist($entity);
} else {
throw new BadRequestHttpException('An error occurs. Please do try later.');
}
}
}
How can I hide the "deleted" items from collection on GET verb (filter them from the result so that they aren't visible) ?
Here is my operation for GET verb, I don't know how to handle this :
class ConferenceListAction extends BaseAction
{
public function __invoke(Request $request, $data)
{
return $data;
}
}
I did something; I'm not sure it's a best pratice.
Since when we do :
return $data;
in our controller, API Platform has already fetch data and fill $data with.
So I decided to add my logic before the return; like :
public function __invoke(Request $request, $data)
{
$cleanDatas = [];
/**
* #var Conference $conf
*/
foreach ($data as $conf) {
if (!$conf->getDeleted()) {
$cleanDatas[] = $conf;
}
}
return $cleanDatas;
}
So now I only have undeleted items. Feel free to let me know if there is something better.
Thanks.
Custom controllers are discouraged in the docs. You are using Doctrine ORM so you can use a Custom Doctrine ORM Extension:
// api/src/Doctrine/ConferenceCollectionExtension.php
namespace App\Doctrine;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Conference;
use Doctrine\ORM\QueryBuilder;
final class CarCollectionExtension implements QueryCollectionExtensionInterface
{
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
{
if ($resourceClass != Conference::class) return;
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere("$rootAlias.isDeleted = false OR $rootAlias.isDeleted IS NULL);
}
}
This will automatically be combined with any filters, sorting and pagination of collection operations with method GET.
You can make this Extension specific to an operation by adding to the if statement something like:
|| $operationName == 'conference_list'
If you're not using the autoconfiguration, you have to register the custom extension:
# api/config/services.yaml
services:
# ...
'App\Doctrine\ConferenceCollectionExtension':
tags:
- { name: api_platform.doctrine.orm.query_extension.collection }
If you also want to add a criterium for item operations, see the docs on Extensions

Image upload with sonata admin

See EDIT above
I think the issue is pretty simple to solve but I can't find any clear answer right now. I hope you might have an idea.
I'm trying to upload an image with sonata admin.
In my entity I have this field
/**
* #ORM\Column(type="string", length=2000)
* #Assert\File(mimeTypes={ "image/png", "image/jpeg" })
*/
private $image;
When I go to the sonata admin form view. The button Upload file is there and defined as below
$formMapper->add('image', FileType::class);
But when I try to send the form, I'm getting this error
The form's view data is expected to be an instance of class Symfony\Component\HttpFoundation\File\File, but is a(n) string. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) string to an instance of Symfony\Component\HttpFoundation\File\File.
I hint this is due to the doctrine string type. But I don't think doctrine has a "File" type.
Thanks for your help.
EDIT:
Considering the link provided in comment, here is the new error
The current field image is not linked to an admin. Please create one for the target entity : ``
<?php
namespace App\Entity;
// src/Entity/Image.php
class Image{
const SERVER_PATH_TO_IMAGE_FOLDER = '/public/images';
/**
* Unmapped property to handle file uploads
*/
private $file;
/**
* #param UploadedFile $file
*/
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
}
/**
* #return UploadedFile
*/
public function getFile()
{
return $this->file;
}
/**
* Manages the copying of the file to the relevant place on the server
*/
public function upload()
{
// the file property can be empty if the field is not required
if (null === $this->getFile()) {
return;
}
// we use the original file name here but you should
// sanitize it at least to avoid any security issues
// move takes the target directory and target filename as params
$this->getFile()->move(
self::SERVER_PATH_TO_IMAGE_FOLDER,
$this->getFile()->getClientOriginalName()
);
// set the path property to the filename where you've saved the file
$this->filename = $this->getFile()->getClientOriginalName();
// clean up the file property as you won't need it anymore
$this->setFile(null);
}
/**
* Lifecycle callback to upload the file to the server.
*/
public function lifecycleFileUpload()
{
$this->upload();
}
/**
* Updates the hash value to force the preUpdate and postUpdate events to fire.
*/
public function refreshUpdated()
{
$this->setUpdated(new \DateTime());
}
// ... the rest of your class lives under here, including the generated fields
// such as filename and updated
}
In my ForumAdmin, now I have
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper->add('name', TextType::class);
$formMapper->add('description', TextAreaType::class);
$formMapper->add('weight', IntegerType::class);
$formMapper->add('category', EntityType::class, [
'class' => Category::class,
'choice_label' => 'name',
]);
$formMapper
->add('image', AdminType::class)
;
}
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add('name');
$datagridMapper->add('category');
}
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->addIdentifier('name');
$listMapper->addIdentifier('description');
$listMapper->addIdentifier('category');
$listMapper->addIdentifier('weight');
$listMapper->addIdentifier('createdAt');
$listMapper->addIdentifier('updatedAt');
$listMapper->addIdentifier('image');
}
public function prePersist($object)
{
parent::prePersist($object);
$this->manageEmbeddedImageAdmins($object);
if($object instanceof Forum){
$object->setCreatedAt(new \DateTime('now'));
$object->setUpdatedAt(new \DateTime('now'));
$object->setStatus("NO_NEW");
}
}
public function preUpdate($page)
{
$this->manageEmbeddedImageAdmins($page);
}
private function manageEmbeddedImageAdmins($page)
{
// Cycle through each field
foreach ($this->getFormFieldDescriptions() as $fieldName => $fieldDescription) {
// detect embedded Admins that manage Images
if ($fieldDescription->getType() === 'sonata_type_admin' &&
($associationMapping = $fieldDescription->getAssociationMapping()) &&
$associationMapping['targetEntity'] === 'App\Entity\Image'
) {
$getter = 'get'.$fieldName;
$setter = 'set'.$fieldName;
/** #var Image $image */
$image = $page->$getter();
if ($image) {
if ($image->getFile()) {
// update the Image to trigger file management
$image->refreshUpdated();
} elseif (!$image->getFile() && !$image->getFilename()) {
// prevent Sf/Sonata trying to create and persist an empty Image
$page->$setter(null);
}
}
}
}
}
I also have this ImageAdmin even if I don't see why it would be usefull
final class ImageAdmin extends AbstractAdmin{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('image', FileType::class);
}
public function prePersist($image)
{
$this->manageFileUpload($image);
}
public function preUpdate($image)
{
$this->manageFileUpload($image);
}
private function manageFileUpload($image)
{
if ($image->getFile()) {
$image->refreshUpdated();
}
}
// ...
}
The current field image is not linked to an admin. Please create one
for the target entity : ``
To fix this error you need add the service in file sonata_admin.yaml like this:
services:
...
admin.image:
class: App\Admin\ImageAdmin
arguments: [~, App\Entity\File, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: 'admin.image' }
public: true

Symfony2 custom Voter: cannot have access to getDoctrine from inside the Voter

I'm trying to implement a custom Voter.
From the controller I call it this way:
$prj = $this->getDoctrine()->getRepository('AppBundle:Project')->findOneById($id);
if (false === $this->get('security.authorization_checker')->isGranted('responsible', $prj)) {
throw new AccessDeniedException('Unauthorised access!');
}
The first line properly retrieves the Project object (I checked with a dump).
The problem occurs inside the voter
<?php
namespace AppBundle\Security\Authorization\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class ProjectVoter implements VoterInterface
{
const RESPONSIBLE = 'responsible';
const ACCOUNTABLE = 'accountable';
const SUPPORT = 'support';
const CONSULTED = 'consulted';
const INFORMED = 'informed';
public function supportsAttribute($attribute)
{
return in_array($attribute, array(
self::RESPONSIBLE,
self::ACCOUNTABLE,
self::SUPPORT,
self::CONSULTED,
self::INFORMED,
));
}
public function supportsClass($class)
{
$supportedClass = 'AppBundle\Entity\Project';
return $supportedClass === $class || is_subclass_of($class, $supportedClass);
}
/**
* #var \AppBundle\Entity\Project $project
*/
public function vote(TokenInterface $token, $project, array $attributes)
{
// check if class of this object is supported by this voter
if (!$this->supportsClass(get_class($project))) {
return VoterInterface::ACCESS_ABSTAIN;
}
// check if the voter is used correct, only allow one attribute
// this isn't a requirement, it's just one easy way for you to
// design your voter
if (1 !== count($attributes)) {
throw new \InvalidArgumentException(
'Only one attribute is allowed'
); //in origin it was 'for VIEW or EDIT, which were the supported attributes
}
// set the attribute to check against
$attribute = $attributes[0];
// check if the given attribute is covered by this voter
if (!$this->supportsAttribute($attribute)) {
return VoterInterface::ACCESS_ABSTAIN;
}
// get current logged in user
$user = $token->getUser();
// make sure there is a user object (i.e. that the user is logged in)
if (!$user instanceof UserInterface) {
return VoterInterface::ACCESS_DENIED;
}
$em = $this->getDoctrine()->getManager();
$projects = $em->getRepository('AppBundle:Project')->findPrjByUserAndRole($user, $attribute);
foreach ($projects as $key => $prj) {
if ($prj['id'] === $project['id'])
{
$granted = true;
$index = $key; // save the index of the last time a specifif project changed status
}
}
if($projects[$index]['is_active']===true) //if the last status is active
return VoterInterface::ACCESS_GRANTED;
else
return VoterInterface::ACCESS_DENIED;
}
}
I get the following error
Attempted to call method "getDoctrine" on class
"AppBundle\Security\Authorization\Voter\ProjectVoter".
I understand that the controller extends Controller, that is why I can use "getDoctrine" there. How can I have access to my DB from inside the Voter?
I solved it. This is pretty curious: I spend hours or days on a problem, then post a question here, and I solve it myself within an hour :/
I needed to add the following in my voter class:
public function __construct(EntityManager $em)
{
$this->em = $em;
}
I needed to add the following on top:
use Doctrine\ORM\EntityManager;
I also needed to add the arguments in the service.yml
security.access.project_voter:
class: AppBundle\Security\Authorization\Voter\ProjectVoter
arguments: [ #doctrine.orm.entity_manager ]
public: false
tags:
- { name: security.voter }

Symfony2 and ParamConverter(s)

Accessing my route /message/new i'm going to show a form for sending a new message to one or more customers. Form model has (among others) a collection of Customer entities:
class MyFormModel
{
/**
* #var ArrayCollection
*/
public $customers;
}
I'd like to implement automatic customers selection using customers GET parameters, like this:
message/new?customers=2,55,543
This is working now by simply splitting on , and do a query for getting customers:
public function newAction(Request $request)
{
$formModel = new MyFormModel();
// GET "customers" parameter
$customersIds = explode($request->get('customers'), ',');
// If something was found in "customers" parameter then get entities
if(!empty($customersIds)) :
$repo = $this->getDoctrine()->getRepository('AcmeHelloBundle:Customer');
$found = $repo->findAllByIdsArray($customersIds);
// Assign found Customer entities
$formModel->customers = $found;
endif;
// Go on showing the form
}
How can i do the same using Symfony 2 converters? Like:
public function newAction(Request $request, $selectedCustomers)
{
}
Answer to my self: there is not such thing to make you life easy. I've coded a quick and dirty (and possibly buggy) solution i'd like to share, waiting for a best one.
EDIT WARNING: this is not going to work with two parameter converters with the same class.
Url example
/mesages/new?customers=2543,3321,445
Annotations:
/**
* #Route("/new")
* #Method("GET|POST")
* #ParamConverter("customers",
* class="Doctrine\Common\Collections\ArrayCollection", options={
* "finder" = "getFindAllWithMobileByUserQueryBuilder",
* "entity" = "Acme\HelloBundle\Entity\Customer",
* "field" = "id",
* "delimiter" = ",",
* }
* )
*/
public function newAction(Request $request, ArrayCollection $customers = null)
{
}
Option delimiter is used to split GET parameter while id is used for adding a WHERE id IN... clause. There are both optional.
Option class is only used as a "signature" to tell that converter should support it. entity has to be a FQCN of a Doctrine entity while finder is a repository method to be invoked and should return a query builder (default one provided).
Converter
class ArrayCollectionConverter implements ParamConverterInterface
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
function apply(Request $request, ConfigurationInterface $configuration)
{
$name = $configuration->getName();
$options = $this->getOptions($configuration);
// Se request attribute to an empty collection (as default)
$request->attributes->set($name, new ArrayCollection());
// If request parameter is missing or empty then return
if(is_null($val = $request->get($name)) || strlen(trim($val)) === 0)
return;
// If splitted values is an empty array then return
if(!($items = preg_split('/\s*'.$options['delimiter'].'\s*/', $val,
0, PREG_SPLIT_NO_EMPTY))) return;
// Get the repository and logged user
$repo = $this->getEntityManager()->getRepository($options['entity']);
$user = $this->getSecurityContext->getToken()->getUser();
if(!$finder = $options['finder']) :
// Create a new default query builder with WHERE user_id clause
$builder = $repo->createQueryBuilder('e');
$builder->andWhere($builder->expr()->eq("e.user", $user->getId()));
else :
// Call finder method on repository
$builder = $repo->$finder($user);
endif;
// Edit the builder and add WHERE IN $items clause
$alias = $builder->getRootAlias() . "." . $options['field'];
$wherein = $builder->expr()->in($alias, $items);
$result = $builder->andwhere($wherein)->getQuery()->getResult();
// Set request attribute and we're done
$request->attributes->set($name, new ArrayCollection($result));
}
public function supports(ConfigurationInterface $configuration)
{
$class = $configuration->getClass();
// Check if class is ArrayCollection from Doctrine
if('Doctrine\Common\Collections\ArrayCollection' !== $class)
return false;
$options = $this->getOptions($configuration);
$manager = $this->getEntityManager();
// Check if $options['entity'] is actually a Dcontrine one
try
{
$manager->getClassMetadata($options['entity']);
return true;
}
catch(\Doctrine\ORM\Mapping\MappingException $e)
{
return false;
}
}
protected function getOptions(ConfigurationInterface $configuration)
{
return array_replace(
array(
'entity' => null,
'finder' => null,
'field' => 'id',
'delimiter' => ','
),
$configuration->getOptions()
);
}
/**
* #return \Doctrine\ORM\EntityManager
*/
protected function getEntityManager()
{
return $this->container->get('doctrine.orm.default_entity_manager');
}
/**
* #return \Symfony\Component\Security\Core\SecurityContext
*/
protected function getSecurityContext()
{
return $this->container->get('security.context');
}
}
Service definition
arraycollection_converter:
class: Acme\HelloBundle\Request\ArrayCollectionConverter
arguments: ['#service_container']
tags:
- { name: request.param_converter}
It's late, but according to latest documentation about #ParamConverter, you can achieve it follow way:
* #ParamConverter("users", class="AcmeBlogBundle:User", options={
* "repository_method" = "findUsersByIds"
* })
you just need make sure that repository method can handle comma (,) separated values

Resources