I'm building a Symfony 2 / Doctrine 2 application on top of an existing MySQL database. Due to poor decisions in the past, i'm stuck with references that are concatenated in a table-column. Unfortunately remodeling the database is not an option.
E.g. entity "Product" referring to multiple "Categories":
| id | name | category_ids |
|----|-----------|--------------|
| 1 | product a | 1,2,5 |
| 2 | product b | 3,4,1 |
| 3 | product c | 2 |
I would like to have the method getCategories available in my "Product" entity which would return a Collection of Category objects.
Is there any way to achieve this with Doctrine?
Maybe use custom code that is based on "FIND_IN_SET"?
SELECT c.*
FROM product p
LEFT OUTER JOIN category c ON FIND_IN_SET(c.id, p.category_ids)
WHERE p.id=:product_id;
Or maybe define the association with exploded values?
explode(',',$this->category_ids)
I try to avoid having to use the EntityManager each time i need to retrieve Categories from my Product entity. Because:
Injecting the EntityManager in Entities is bad practice
Using the EntityManager each time in my controllers a bit against the DRY-principle
I have no idea how to achieve this in Symfony FormTypes to have a Choice/Entity field with the relevant Categories for a Product.
Solution 1
You can make a hydration strategy for your getCategories method and register this strategy inside your hydrator class (could even be DoctrineObject hydrator). Something like:
Strategy
<?php
namespace My\Hydrator\Strategy;
use Doctrine\Common\Collections\ArrayCollection;
use My\Entity\Category;
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Persistence\ObjectManagerAwareInterface;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
class CategoriesStrategy implements StrategyInterface, ObjectManagerAwareInterface
{
/**
* #var ObjectManager
*/
protected $objectManager;
/**
* #param ObjectManager $objectManager
* #param String $hostName
*/
public function __construct(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
/**
* #param array $value
* #return ArrayCollection
*/
public function extract($value)
{
$collection = new ArrayCollection();
if (is_array($value)) {
foreach ($value as $id) {
$category = $this->getObjectManager()->find(Category::class, $id);
$collection->add($category);
}
}
return $collection;
}
/**
* #param ArrayCollection $value
* #return array
*/
public function hydrate($value)
{
$array = array();
/** #var Category $category */
foreach ($value as $category) {
$array[] = $category->getId();
}
return $array;
}
/**
* #param ObjectManager $objectManager
* #return $this
*/
public function setObjectManager(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
return $this;
}
/**
* #return ObjectManager
*/
public function getObjectManager()
{
return $this->objectManager;
}
}
You probably need a factory to register your CategoriesStrategy inside your hydrator class:
Hydrator Factory
<?php
namespace My\Hydrator;
use Doctrine\Common\Persistence\ObjectManager;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject;
use My\Hydrator\Strategy\CategoriesStrategy;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\ServiceManager;
class MyHydratorFactory implements FactoryInterface
{
/**
* #param ServiceLocatorInterface $serviceLocator
* #return DoctrineObject
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
/** #var ServiceManager $serviceManager */
$serviceManager = $serviceLocator->getServiceLocator();
/** #var ObjectManager $objectManager */
$objectManager = $serviceManager->get('bc_object_manager');
/** #var DoctrineObject $hydrator */
$hydrator = new DoctrineObject($objectManager);
$hydrator->addStrategy('categories', new CategoriesStrategy($objectManager));
return $hydrator;
}
}
This is not tested, but you get the idea...
Solution 2
Another solution would be to register a DBAL type for your categories. You can check on how to do this in the Doctrine2 documentation chapter 8.4. Custom Mapping Types.
In your entity column definition you point to a categories type:
/**
* #var string
* #ORM\Column(type="categories")
*/
protected $categories;
And the magic you register in doctrine like this:
'doctrine' => array(
'configuration' => array(
'orm_default' => array(
'types' => array(
'categories' => 'My\DBAL\Types\CategoriesCollection '
)
)
)
)
And then the class itself:
<?php
namespace My\DBAL\Types;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Common\Collections\Collection;
class CategoriesCollection extends \Doctrine\DBAL\Types\Type
{
const NAME = 'categories';
/**
* #return string
*/
public function getName()
{
return self::NAME;
}
/**
* {#inheritdoc}
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getDoctrineTypeMapping('simple_array');
}
/**
* #param Collection $collection
* #param AbstractPlatform $platform
* #return array
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
$array = [];
foreach($value as $category)
{
$category_id = $category->getId();
array_push($array, $category_id);
}
return $array;
}
/**
* {#inheritdoc}
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
$collection = new ArrayCollection();
if ($value === null) {
return $collection;
}
foreach($value as $category_id){
$category = $this->em->getReference('Vendor\Bundle\Entity\Category', $category_id);
$collection->add($category);
}
return $collection;
}
/**
* #var EntityManager
*/
protected $em;
/**
* #param EntityManager $entityManager
*/
public function setEntityManager(EntityManager $entityManager)
{
$this->em = $entityManager;
}
}
This solution is virtually the same as the other solution only you use the Doctrine2 internals to get there. You will still need to register the EntityManager in your DBAL type and not sure what is the easiest way, so that I leave up to you.
In Symfony you can register the Custom Mapping Type in your app/config/config.yml file
doctrine:
dbal:
types:
category_ids: Vendor\Bundle\Type\CategoriesCollection
You can than inject the EntityManager dependency in the boot sequence of your bundle:
<?php
namespace Vendor\Bundle\Bundle;
use Doctrine\DBAL\Types\Type;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Trilations\TApp\CoreBundle\Type\CategoryCollectionType;
class VendorBundleBundle extends Bundle
{
public function boot()
{
$em = $this->container->get('doctrine.orm.default_entity_manager');
$categoryCollectionType = Type::getType('category_ids');
$categoryCollectionType->setEntityManager($em);
}
}
And map the field to the correct custom mapping:
Vendor\Bundle\Enitity\Product
table: product
fields:
categories: category_ids
Related
I'm trying to write a unit test for the startedAt() method using mocks however the problem I'm facing is that I don't think I can access the builder instance from inside that startedAt() method.
To test the startedAt() method I created a fixture class called ExampleFilters and had it extend the parent class of Filters. Inside of the ExampleFilters class I import the FiltersByStartDate trait.
Does anyone have any suggestions on how I can access the builder property from the FiltersByStartDate trait?
Any ideas on this?
<?php
namespace App\Filters\Concerns;
trait FiltersByStartDate
{
/**
* Filter a query to include models of a specific date started.
*
* #param array $startedAt
* #return \Illuminate\Database\Eloquent\Builder
*/
public function startedAt($startedAt)
{
if (isset($startedAt[1])) {
$this->builder->whereHas('currentEmployment', function ($query) use ($startedAt) {
$query->whereBetween('started_at', [
$startedAt[0],
$startedAt[1]
]);
});
} else {
$this->builder->whereHas('currentEmployment', function ($query) use ($startedAt) {
$query->whereDate('started_at', $startedAt[0]);
});
}
return $this->builder;
}
}
<?php
namespace Tests\Fixtures;
use App\Filters\Concerns\FiltersByStartDate;
use App\Filters\Filters;
class ExampleFilters extends Filters
{
use FiltersByStartDate;
}
<?php
namespace App\Filters;
use Illuminate\Http\Request;
abstract class Filters
{
/**
* #var \Illuminate\Http\Request
*/
protected $request;
/**
* The Eloquent builder.
*
* #var \Illuminate\Database\Eloquent\Builder
*/
protected $builder;
/**
* Registered filters to operate upon
*
* #var array
*/
protected $filters = [];
/**
* Create a new class instance.
*
* #param \Illuminate\Http\Request $request
*/
public function __construct(Request $request)
{
$this->request = $request;
}
/**
* Apply the filters.
*
* #param \Illuminate\Database\Eloquent\Builder $builder
* #return \Illuminate\Database\Eloquent\Builder
*/
public function apply($builder)
{
$this->builder = $builder;
foreach ($this->getFilters() as $filter => $value) {
if (method_exists($this, $filter)) {
$this->$filter($value);
}
}
return $this->builder;
}
/**
* Fetch all relevant filters from the request.
*
* #return array
*/
public function getFilters()
{
return array_filter($this->request->only($this->filters));
}
}
<?php
namespace Tests\Unit\Filters\Concerns;
use Illuminate\Database\Query\Builder;
use Tests\Fixtures\ExampleFilters;
use Tests\TestCase;
/*
* #group filters
*/
class FiltersByStartDateTest extends TestCase
{
/* #var Tests\Fixtures\ExampleFilters */
protected $subject;
public function setUp(): void
{
$this->subject = app(ExampleFilters::class);
}
/** #test */
public function models_can_be_filtered_by_their_start_date()
{
// $this->markTestIncomplete();
$dateSet = ['2020-01-01 00:00:00'];
$mock = \Mockery::mock(Builder::class)
->shouldReceive('whereHas', \Mockery::any())
->shouldReceive('whereDate')
->withArgs(['started_at', $dateSet])
->once()
->andReturn(true)
->getMock();
dd($this->subject->startedAt($dateSet));
$builderMockFromDate = $this->subject->startedAt($dateSet);
$this->assertSame($builderMockFromDate, $mock);
}
}
I'm currently migrating a ZF2 application to ZF3.
Mostly everything is going smoothly but I'm stuck on one thing.
In my Module.php, I have an ACL management using zend-permissions-acl.
class Module
{
protected $defaultLang = 'fr';
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
if (!$e->getRequest() instanceof ConsoleRequest){
$eventManager->attach(MvcEvent::EVENT_RENDER_ERROR, array($this, 'onRenderError'));
$eventManager->attach(MvcEvent::EVENT_RENDER, array($this, 'onRender'));
$eventManager->attach(MvcEvent::EVENT_FINISH, array($this, 'onFinish'));
$this->initAcl($e);
$eventManager->attach('route', array($this, 'checkAcl'));
}
}
public function checkAcl(MvcEvent $e) {
$app = $e->getApplication();
$sm = $app->getServiceManager();
$route = $e -> getRouteMatch() -> getMatchedRouteName();
$authService = $sm->get('AuthenticationService');
$jwtService = $sm->get('JwtService');
$translator = $sm->get('translator');
$identity = null;
try {
$identity = $jwtService->getIdentity($e->getRequest());
} catch(\Firebase\JWT\ExpiredException $exception) {
$response = $e->getResponse();
$response->setStatusCode(401);
return $response;
}
if(is_null($identity) && $authService->hasIdentity()) { // no header being passed on... we try to use standard validation
$authService->setJwtMode(false);
$identity = $authService->getIdentity();
}
$userRole = 'default';
$translator->setLocale($this->defaultLang);
if(!is_null($identity))
{
$userRole = $identity->getType();
//check if client or prospect
if($userRole >= User::TYPE_CLIENT)
{
$userManagementRight = UserRight::CREATE_USERS;
if($identity->hasRight($userManagementRight))
$userRole = 'userManagement';
}
$translator->setLocale($identity->getLang());
}
if (!$e->getViewModel()->acl->isAllowed($userRole, null, $route)) {
$response = $e -> getResponse();
$response->setStatusCode(403);
return $response;
}
public function initAcl(MvcEvent $e) {
//here is list of routes allowed
}
}
My issue here is that I'm still using the getServiceManager and therefore getting the deprecated warning : Usage of Zend\ServiceManager\ServiceManager::getServiceLocator is deprecated since v3.0.0;
Basically, I just need to inject dependencies into Module.php.
I guess otherwise I would have to move the checkAcl to the Controller directly and inject the ACL in them ? Not sure what is the proper way of doing this.
Any feedback on this would be greatly appreciated.
Regards,
Robert
To solve the issue you should use a Listener class and Factory. It would also help you with more separation of concerns :)
You seem quite capable of figuring stuff out, judging by your code. As such, I'm just going to give you an example of my own, so you should fill yours in with your own code (I'm also a bit lazy and do not wish to rewrite everything when I can copy/paste my code in ;) )
In your module.config.php:
'listeners' => [
// Listing class here will automatically have them "activated" as listeners
ActiveSessionListener::class,
],
'service_manager' => [
'factories' => [
// The class (might need a) Factory
ActiveSessionListener::class => ActiveSessionListenerFactory::class,
],
],
The Factory
<?php
namespace User\Factory\Listener;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
use Interop\Container\ContainerInterface;
use User\Listener\ActiveSessionListener;
use Zend\Authentication\AuthenticationService;
use Zend\ServiceManager\Factory\FactoryInterface;
class ActiveSessionListenerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var ObjectManager $entityManager */
$entityManager = $container->get(EntityManager::class);
/** #var AuthenticationService $authenticationService */
$authenticationService = $container->get(AuthenticationService::class);
return new ActiveSessionListener($authenticationService, $entityManager);
}
}
The Listener
<?php
namespace User\Listener;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
use User\Entity\User;
use Zend\Authentication\AuthenticationService;
use Zend\EventManager\Event;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\Mvc\MvcEvent;
/**
* Class ActiveSessionListener
*
* #package User\Listener
*
* Purpose of this class is to make sure that the identity of an active session becomes managed by the EntityManager.
* A User Entity must be in a managed state in the event of any changes to the Entity itself or in relations to/from it.
*/
class ActiveSessionListener implements ListenerAggregateInterface
{
/**
* #var AuthenticationService
*/
protected $authenticationService;
/**
* #var ObjectManager|EntityManager
*/
protected $objectManager;
/**
* #var array
*/
protected $listeners = [];
/**
* CreatedByUserListener constructor.
*
* #param AuthenticationService $authenticationService
* #param ObjectManager $objectManager
*/
public function __construct(AuthenticationService $authenticationService, ObjectManager $objectManager)
{
$this->setAuthenticationService($authenticationService);
$this->setObjectManager($objectManager);
}
/**
* #param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$events->attach(MvcEvent::EVENT_ROUTE, [$this, 'haveDoctrineManagerUser'], 1000);
}
/**
* #param Event $event
*
* #throws \Doctrine\Common\Persistence\Mapping\MappingException
* #throws \Doctrine\ORM\ORMException
*/
public function haveDoctrineManagerUser(Event $event)
{
if ($this->getAuthenticationService()->hasIdentity()) {
// Get current unmanaged (by Doctrine) session User
$identity = $this->getAuthenticationService()->getIdentity();
// Merge back into a managed state
$this->getObjectManager()->merge($identity);
$this->getObjectManager()->clear();
// Get the now managed Entity & replace the unmanaged session User by the managed User
$this->getAuthenticationService()->getStorage()->write(
$this->getObjectManager()->find(User::class, $identity->getId())
);
}
}
/**
* #return AuthenticationService
*/
public function getAuthenticationService() : AuthenticationService
{
return $this->authenticationService;
}
/**
* #param AuthenticationService $authenticationService
*
* #return ActiveSessionListener
*/
public function setAuthenticationService(AuthenticationService $authenticationService) : ActiveSessionListener
{
$this->authenticationService = $authenticationService;
return $this;
}
/**
* #return ObjectManager|EntityManager
*/
public function getObjectManager()
{
return $this->objectManager;
}
/**
* #param ObjectManager|EntityManager $objectManager
*
* #return ActiveSessionListener
*/
public function setObjectManager($objectManager)
{
$this->objectManager = $objectManager;
return $this;
}
}
The important bits:
The Listener class must implement ListenerAggregateInterface
Must be activated in the listeners key of the module configuration
That's it really. You then have the basic building blocks for a Listener.
Apart from the attach function you could take the rest and make that into an abstract class if you'd like. Would save a few lines (read: duplicate code) with multiple Listeners.
NOTE: Above example uses the normal EventManager. With a simple change to the above code you could create "generic" listeners, by attaching them to the SharedEventManager, like so:
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(SomeClass::class, EventConstantClass::SOME_STRING_CONSTANT, [$this, 'callbackFunction']);
}
public function callbackFunction (MvcEvent $event) {...}
i want to implement a new doctrine data type (http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html).
I have implemented an Country Service which loads country data via adapter from any library. Know i have the following implementation:
<?php
interface CountryInterface;
interface Address
{
public function setCountry(CountryInterface $country);
public function getCountry() : CountryInterface;
}
?>
So, what I want to do is - make a CountryType which converts the Country Object to an specific string value (used field will be set via OptionClass, ex.: Alpha2, Alpha3, IsoNumber).
My problem is, doctrine only allows data types mapping via classname, so I can't implement an factory to load all needed dependencies.
I hope this is understandable.
regards
First you will need to register your custom DBAL type for country extending the Doctrine\DBAL\Types\Type class:
<?php
namespace Application\DBAL\Types;
use Application\Resource\Country;
use Application\Service\CountryService;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use InvalidArgumentException;
class CountryType extends Type
{
const NAME = 'country';
/**
* Country service
*/
protected $countryService;
/**
* #return string
*/
public function getName()
{
return self::NAME;
}
/**
* {#inheritdoc}
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getDoctrineTypeMapping('text');
}
/**
* {#inheritdoc}
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if($value === null){
return null;
}
if ($value instanceof Country) {
return (string) $value;
}
throw ConversionException::conversionFailed($value, self::NAME);
}
/**
* {#inheritdoc}
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if($value === null){
return null;
}
$country = $this->countryService->getCountry($value);
if( ! $country instanceof Country ){
throw ConversionException::conversionFailed($value, self::NAME);
}
return $country;
}
/**
* Set country service
*
* #var CountryService $service
*/
public function setCountryService ($service){
$this->countryService = $service;
}
}
This type needs to implement four methods getName, getSQLDeclaration, convertToDatabaseValue and convertToPHPValue.
Thre first one returns the name for the type
The second one is for the type declaration in the (SQL) database (I used text in the example, but you can also use integer or any other valid doctrine database type).
The third method converts your country object to a database value (so in this case a text value).
The last method does the opposite; it converts the text value from the database. In this case I just instantiate the Country class and pass the database value to the constructor. You need to add your custom logic inside your class constructor.
In my example I assume that null values are also allowed.
A simple version of your Country class could look like this:
<?php
namespace Application\Resource;
class Country{
protected $value;
/**
* Magic stringify to cast country object to a string
*/
public function __toString(){
return $value;
}
/**
* Constructor method
*/
public function construct($value){
$this->value = $value
// set other properties...
}
// setters and getters...
}
It is up to you whether value should be alpha2/alpha3/country_name or whatever you want visible in the database. You should somehow also populate the other country with the other properties in the constructor method. I leave this part up to you.
Now you need to register your custom country type so doctrine will use it:
'doctrine' => array(
//...
'configuration' => array(
'orm_default' => array(
Application\DBAL\Types\CountryType::NAME => Application\DBAL\Types\CountryType::class,
)
)
)
And you can set your service on bootstrap in your application Module.php file:
/**
* #param EventInterface|MvcEvent $event
* #return void
*/
public function onBootstrap(EventInterface $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager();
$eventManager->attach(MvcEvent::EVENT_BOOTSTRAP, array($this, 'initializeCountryType');
}
/**
* #param MvcEvent $event
*/
public function initializeCountryType(MvcEvent $event)
{
$application = $event->getApplication();
$serviceManager = $application->getServiceManager();
//get your country service from service manager
$countryService = $serviceManager->getCountryService();
$countryType = \Doctrine\DBAL\Types\Type::getType('country');
$countryType->setCountryService($countryService);
}
Now you can use your country type in any entity definition as follows:
/**
* #var string
* #ORM\Column(type="country", nullable=true)
*/
protected $country;
Read more on how to map custom DBAL types in the Doctrine2 documentation chapter Custom Mapping Types
I have a lot of Categories in database.
Here is Category Entity
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity
* #ORM\Table(name="categories")
*/
class Category
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Category")
*/
protected $rootCategory;
/**
* #ORM\Column(type="text")
*/
protected $name;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Set rootCategory
*
* #param \AppBundle\Entity\Category $rootCategory
*
* #return Category
*/
public function setRootCategory(\AppBundle\Entity\Category $rootCategory = null)
{
$this->rootCategory = $rootCategory;
return $this;
}
/**
* Get rootCategory
*
* #return \AppBundle\Entity\Category
*/
public function getRootCategory()
{
return $this->rootCategory;
}
}
I want to get all categories in my edit form
EditFormType:
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use AppBundle\Controller\CategoryController;
class EditPhotoFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$categoryController = new CategoryController();
$builder->add('title', 'text');
$builder->add('description', 'textarea');
$builder->add('category', EntityType::class, array(
'class' => 'AppBundle:Category',
'choices' => $categoryController->getCategories(),
));
}
public function getName()
{
return 'app_photo_edit';
}
}
getCategories()
public function getCategories() {
$em = $this->getDoctrine()->getManager();
return $em->getRepository('AppBundle:Category')->findAll();
}
I am getting next error:
Error: Call to a member function has() on null
Thats because there is not Doctrine in controller object. Where should i get Doctrine and Repository in this case?
How should i do it correct way?
First, you should NEVER instantiate any Controller class yourself. Controller classes are used by Symfony's Kernel to handle a request, and they are loaded automatically with dependencies to do so.
Right here, you don't even need to require the EntityManager in your FormType, because EntityType has a built-in option query_builder to do what you need:
$builder->add('category', EntityType::class, array(
'class' => 'AppBundle:Category',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c');
},
);
This should do the trick. (check here for more details)
However, if one day you really need to import a dependancy inside your Form (whether it is EntityManager or another service), here's how you should do:
A. import the given dependency in your constructor:
private $dependency;
public function __construct(Dependency $dependency)
{
$this->$dependency = $dependency;
}
B. Declare your Form as a Service, with your dependency's id as argument:
<service id="app.form.type.edit_photo"
class="AppBundle\Form\Type\EditPhotoFormType">
<tag name="form.type" />
<argument type="service" id="app.dependencies.your_dependency" />
</service>
Then use $this->dependency in your Form wherever you need.
Hope this helps! :)
I have a form builded by formbuilder
public function buildForm(FormBuilderInterface $builder, array $options){
$query = $this->em->createQueryBuilder();
$query->select('sn.serial_nr')
->from('KopictAdminBundle:SerialNumber', 'sn');
$serialnumbers = $query->getQuery()->getResult();
$options = array();
foreach($serialnumbers as $serialnumber){
$options[$serialnumber['serial_nr']] = $serialnumber['serial_nr'];
}
$builder->add("serial_nr","text");
}
It shows the form correctly but when i submit it I get this error:
"The form's view data is expected to be an instance of class Kopict\AdminBundle\Entity\SerialNumber, 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 Kopict\AdminBundle\Entity\SerialNumber." at /var/www/kopadmin/vendor/symfony/symfony/src/Symfony/Component/Form/Form.php line 373
This is how my entity looks like:
class SerialNumber
{
/**
* #var integer $id
*/
private $id;
/**
* #var interger $product_revision_id
*/
private $product_revision_id;
/**
* #var interger $booking_id
*/
private $booking_id;
/**
* #var string $serial_nr
*/
public $serial_nr;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set product_revision_id
*
* #param integer $product_revision_id
* #return SerialNumber
*/
public function setProduct_revision_id($product_revision_id)
{
$this->product_revision_id = $product_revision_id;
return $this;
}
/**
* Get product_revision_id
*
* #return integer
*/
public function getProduct_revision_id()
{
return $this->product_revision_id;
}
/**
* Set booking_id
*
* #param integer $booking_id
* #return SerialNumber
*/
public function setBooking_id($booking_id)
{
$this->booking_id = $booking_id;
return $this;
}
/**
* Get booking_id
*
* #return integer
*/
public function getBooking_id()
{
return $this->booking_id;
}
/**
* Set serial_nr
*
* #param string $serial_nr
* #return SerialNumber
*/
public function setSerial_nr($serial_nr)
{
$this->serial_nr = $serial_nr;
return $this;
}
/**
* Get serial_nr
*
* #return string
*/
public function getSerial_nr()
{
return $this->serial_nr;
}
}
I have tried to add the data_class but I can't find the good place to add it because the code keeps giving me te same error.
First of all, you need to make your serial_nr private otherwise no need to have getSerial_nr and setSerial_nr functions. Because you can reach to serial_nr outside of your class without having setters and getters.
Second, why you are adding serial numbers into options field?
Assuming you want to have serial numbers as a choice field. I have a solution for you.
Usually entities are related in Doctrine ORM as many-to-one one-to-many. In that case its very simple to get related fields as a choice field. For this case symfony has a built in entity field.
SerialNumberType - form type class. (You have to change this name to yours)
<?php
namespace Kopict\AdminBundle\Form;
use Doctrine\ORM\em;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class SerialNumberType extends AbstractType
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$query = $this->em->createQueryBuilder();
$query->select('sn.serial_nr')->from('KopictAdminBundle:SerialNumber', 'sn');
$serialNumbers = $query->getQuery()->getResult();
$choices = array();
foreach ($serialNumbers as $serialNumber) {
$choices[$serialNumber['serial_nr']] = $serialNumber['serial_nr'];
}
$builder->add("serial_nr", "choice", array(
'choices' => $choices,
));
}
public function getName()
{
return 'app_bundle_serial_number_type';
}
}
Inside Controller Action
<?php
namespace Kopict\AdminBundle\Controller;
use Kopict\AdminBundle\Entity\SerialNumber;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DefaultController extends Controller
{
public function indexAction()
{
$serialNumber = new SerialNumber();
$form = $this->createForm($this->get('kopict_admin.form.serialnumber'), $serialNumber);
return $this->render('KopictAdminBundle:Default:index.html.twig', array('form' => $form->createView()));
}
}
services.yml
services:
kopict_admin.form.serialnumber:
class: Kopict\AdminBundle\Form\SerialNumberType
arguments: [ #doctrine.orm.entity_manager ]
scope: request