started with a new ZF2 project an get following error
Argument 1 passed to ZendDeveloperTools\Module::init() must implement interface Zend\ModuleManager\ModuleManagerInterface, null given, called in /vendor/zendframework/zend-modulemanager/src/Listener/InitTrigger.php on line 33 and defined in /vendor/zendframework/zend-developer-tools/src/ZendDeveloperTools/Module.php on line 34
It doesn't matter which Module is first in application.config.php I always got this error.
This is mine Module.php file. The error is saying that you need to pass a ModuleManagerInterface as first parameter in your init method. That init method has an interface. Have a look at the Zend\ModuleManager\Feature\*Interface.php files and you will see your mistake.
namespace Application;
use Zend\Mvc\MvcEvent;
use Zend\EventManager\EventInterface;
use Zend\ModuleManager\Feature\AutoloaderProviderInterface;
use Zend\ModuleManager\Feature\ConfigProviderInterface;
use Zend\ModuleManager\Feature\BootstrapListenerInterface;
use Zend\ModuleManager\Feature\InitProviderInterface;
use Zend\ModuleManager\ModuleManagerInterface;
class Module implements AutoloaderProviderInterface, ConfigProviderInterface, BootstrapListenerInterface, InitProviderInterface
{
/**
* Setup module layout
*
* #param $moduleManager ModuleManager
*/
public function init(ModuleManagerInterface $moduleManager)
{
$moduleManager->getEventManager()->getSharedManager()->attach(__NAMESPACE__, MvcEvent::EVENT_DISPATCH, function (MvcEvent $e) {
$e->getTarget()->layout('layout/layout');
});
}
/**
* Listen to the bootstrap event
*
* #param EventInterface $e
*/
public function onBootstrap(EventInterface $e)
{
}
/**
* #return array|\Traversable
*/
public function getConfig()
{
return include __DIR__.'/config/module.config.php';
}
/**
* Return an array for passing to Zend\Loader\AutoloaderFactory.
*
* #return array
*/
public function getAutoloaderConfig()
{
return [
'Zend\Loader\ClassMapAutoloader' => [
__DIR__.'/autoload_classmap.php',
],
'Zend\Loader\StandardAutoloader' => [
'namespaces' => [
__NAMESPACE__ => __DIR__.'/src/'.__NAMESPACE__,
],
],
];
}
}
Related
I would like to return a different result for a computed field when viewing the index view than when viewing the detail view of a resource.
Basically something like viewIs() below:
Text::make('Preview', function () {
if($this->viewIs('index'){
return \small_preview($this->image);
}
return \large_preview($this->image);
})->asHtml(),
You can check the class of the request:
Text::make('Preview', function () use ($request) {
if ($request instanceof \Laravel\Nova\Http\Requests\ResourceDetailRequest) {
return \large_preview($this->image);
}
return \small_preview($this->image);
});
Otherwise, you can create your own viewIs function:
// app/Nova/Resource.php
/**
* Check the current view.
*
* #param string $view
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #retrun bool
*/
public function viewIs($view, $request)
{
$class = '\Laravel\Nova\Http\Requests\\Resource'.ucfirst($view).'Request';
return $request instanceof $class;
}
Then you can do it like this:
Text::make('Preview', function () use ($request) {
if ($this->viewIs('detail', $request) {
return \large_preview($this->image);
}
return \small_preview($this->image);
});
Unfortunately, #Chin's answer did not work for me as for Edit view the request class is just a basic Laravel\Nova\Http\Request class.
My workaround to check if this is an index view is as follows:
/**
* Check if the current view is an index view.
*
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #return bool
*/
public function isIndex($request)
{
return $request->resourceId === null;
}
The NovaRequest class will soon be able to help, as the isResourceIndexRequest and isResourceDetailRequest are already in master.
As the Nova repo is private I will keep you posted, when it will be usable.
In the meantime I am falling back to helper methods on the Nova Resource class (app/Nova/Resource.php):
namespace App\Nova;
use Laravel\Nova\Http\Requests\ResourceDetailRequest;
use Laravel\Nova\Http\Requests\ResourceIndexRequest;
use Laravel\Nova\Resource as NovaResource;
use Laravel\Nova\Http\Requests\NovaRequest;
abstract class Resource extends NovaResource
{
// [...]
/**
* Determine if this request is a resource index request.
*
* #return bool
*/
public function isResourceIndexRequest($request)
{
return $request instanceof ResourceIndexRequest;
}
/**
* Determine if this request is a resource detail request.
*
* #return bool
*/
public function isResourceDetailRequest($request)
{
return $request instanceof ResourceDetailRequest;
}
}
Usage:
public function fields(Request $request)
{
$fields = [
// [...]
];
if ($this->isResourceDetailRequest($request)) {
if ($this->isResourceDetailRequest($request)) {
$fields = array_merge($fields, [
// [...]
]);
}
}
return $fields;
}
I added this little helper class
namespace App\Helpers;
class CurrentResourceAction {
public static function isIndex($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\ResourceIndexRequest;
}
public static function isDetail($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\ResourceDetailRequest;
}
public static function isCreate($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\NovaRequest &&
$request->editMode === 'create';
}
public static function isUpdate($request)
{
return $request instanceof \Laravel\Nova\Http\Requests\NovaRequest &&
$request->editMode === 'update';
}
}
you can call it anywhere you need to
A bit late but hey! You can check against the NovaRequest properties editing and editMode ('create', 'update', 'attach' etc.)
// Determine if you are creating a model.
$request->editMode == 'create';
Or as they say, "Read the source Luke" and see how they determine it. See the Laravel\Nova\Http\Requests\NovaRequest, it contains similar checks.
namespace Laravel\Nova\Http\Requests;
class NovaRequest extends FormRequest
{
/**
* Determine if this request is via a many to many relationship.
*
* #return bool
*/
public function viaManyToMany();
/**
* Determine if this request is an attach or create request.
*
* #return bool
*/
public function isCreateOrAttachRequest();
/**
* Determine if this request is an update or update-attached request.
*
* #return bool
*/
public function isUpdateOrUpdateAttachedRequest();
/**
* Determine if this request is a resource index request.
*
* #return bool
*/
public function isResourceIndexRequest();
/**
* Determine if this request is a resource detail request.
*
* #return bool
*/
public function isResourceDetailRequest();
/**
* Determine if this request is an action request.
*
* #return bool
*/
public function isActionRequest();
}
Would've been nice if you can type hint the NovaRequest instead of the regular one in the Nova resource fields() method but it is not allowed due to the parent resource being extended.
You can create two separate fields for index & details page.
// ----- For Index page
Text::make('Preview', function () {
return \small_preview($this->image);
})
->onlyOnIndex()
->asHtml(),
// ----- For Detail page
Text::make('Preview', function () {
return \large_preview($this->image);
})
->onlyOnDetail()
->asHtml(),
Did someone succed in translating Sonata Admin entities on Symfony 3 (actually I'm using 3.3).
I tried different solutions, but none really worked.
With gedmo translation, the main problem is that the translations are saved for the differents languages on the database, but then in the admin (list end forms too) the Sonata bundle only dispays the default locale translation, although a different flag/translation is clicked/choosed.
I also tried with KNP tarnslation bundle, and with A2lix translation, but these two have the exat same problem: when you set (in the admin class) a field as "sortable" then in the record list when you try to sort by thet field, Symfony throw an error, because the translation systems try to crete an association with another field tha not exists!
Anyway, staying on the Gedmo soultion, the main problem is that (putting apart the A2lix solution because of the problem I already mentioned) I don't know how set a field as translatable in the admin class (BlogPostAdmin.php) because simply using the config files and the entity and translation class, does not seem to work. The problem, as already said, is that the translations are saved in the database, but are not displayed in the admin lists/forms.
Here are my config and entities files:
AppKernel.php
<?php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new AppBundle\AppBundle(),
/// These are the other bundles the SonataAdminBundle relies on
new Sonata\CoreBundle\SonataCoreBundle(),
new Sonata\BlockBundle\SonataBlockBundle(),
new Knp\Bundle\MenuBundle\KnpMenuBundle(),
new Sonata\TranslationBundle\SonataTranslationBundle(),
// And finally, the storage and SonataAdminBundle
new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
new Sonata\AdminBundle\SonataAdminBundle(),
// stof [used in Sonata translations]
new Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),
// assetic
new Symfony\Bundle\AsseticBundle\AsseticBundle(),
];
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
if ('dev' === $this->getEnvironment()) {
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
$bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle();
}
}
return $bundles;
}
public function getRootDir()
{
return __DIR__;
}
public function getCacheDir()
{
return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
}
public function getLogDir()
{
return dirname(__DIR__).'/var/logs';
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
}
}
config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: it
framework:
#esi: ~
translator: { fallbacks: ['%locale%'] }
secret: '%secret%'
router:
resource: '%kernel.project_dir%/app/config/routing.yml'
strict_requirements: ~
form: ~
csrf_protection: ~
validation: { enable_annotations: true }
#serializer: { enable_annotations: true }
templating:
engines: ['twig']
default_locale: '%locale%'
trusted_hosts: ~
session:
# https://symfony.com/doc/current/reference/configuration/framework.html#handler-id
handler_id: session.handler.native_file
save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
fragments: ~
http_method_override: true
assets: ~
php_errors:
log: true
# Twig Configuration
twig:
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
# Doctrine Configuration
doctrine:
dbal:
driver: pdo_mysql
host: '%database_host%'
port: '%database_port%'
dbname: '%database_name%'
user: '%database_user%'
password: '%database_password%'
charset: UTF8
# if using pdo_sqlite as your database driver:
# 1. add the path in parameters.yml
# e.g. database_path: "%kernel.project_dir%/var/data/data.sqlite"
# 2. Uncomment database_path in parameters.yml.dist
# 3. Uncomment next line:
#path: '%database_path%'
orm:
auto_generate_proxy_classes: '%kernel.debug%'
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
# mappings:
# # Doctrine extensions
# translatable:
# type: annotation
# alias: Gedmo
# prefix: Gedmo\Translatable\Entity
# dir: "%kernel.root_dir%/../vendor/gedmo/doctrine-extensions/lib/Gedmo/Translatable/Entity/MappedSuperclass"
# Swiftmailer Configuration
swiftmailer:
transport: '%mailer_transport%'
host: '%mailer_host%'
username: '%mailer_user%'
password: '%mailer_password%'
spool: { type: memory }
sonata_block:
default_contexts: [cms]
blocks:
# enable the SonataAdminBundle block
sonata.admin.block.admin_list:
contexts: [admin]
sonata_translation:
locales: [it, en]
default_locale: %locale%
# here enable the types you need
gedmo:
enabled: true
# knplabs:
# enabled: true
#phpcr:
# enabled: true
sonata_admin:
templates:
layout: admin/layout.html.twig
assetic:
debug: '%kernel.debug%'
use_controller: '%kernel.debug%'
filters:
cssrewrite: ~
#stof_doctrine_extensions:
# #default_locale: %locale%
# orm:
# default:
# sluggable: true
# timestampable: true
services.yml
# Learn more about services, parameters and containers at
# https://symfony.com/doc/current/service_container.html
parameters:
locale: 'it'
locales: ['it', 'en']
services:
# default configuration for services in *this* file
_defaults:
# automatically injects dependencies in your services
autowire: true
# automatically registers your services as commands, event subscribers, etc.
autoconfigure: true
# this means you cannot fetch services directly from the container via $container->get()
# if you need to do this, you can override this setting on individual services
public: false
# makes classes in src/AppBundle available to be used as services
# this creates a service per class whose id is the fully-qualified class name
AppBundle\:
resource: '../../src/AppBundle/*'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
exclude: '../../src/AppBundle/{Entity,Repository,Tests}'
# controllers are imported separately to make sure they're public
# and have a tag that allows actions to type-hint services
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
public: true
tags: ['controller.service_arguments']
# add more services, or override services that need manual wiring
# AppBundle\Service\ExampleService:
# arguments:
# $someArgument: 'some_value'
admin.category:
class: AppBundle\Admin\CategoryAdmin
arguments: [~, AppBundle\Entity\Category, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: Category }
public: true
admin.blog_post:
class: AppBundle\Admin\BlogPostAdmin
arguments: [~, AppBundle\Entity\BlogPost, ~]
tags:
- { name: sonata.admin, manager_type: orm, label: Blog post }
public: true
# Doctrine Extension listeners to handle behaviors
gedmo.listener.translatable:
class: Gedmo\Translatable\TranslatableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
#- [ setAnnotationReader, [ #annotation_reader ] ]
- [ setDefaultLocale, [ it ] ]
- [ setTranslationFallback, [ false ] ]
- [ setPersistDefaultLocaleTranslation, [ false ] ]
BlogPost.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Sonata\TranslationBundle\Model\Gedmo\AbstractPersonalTranslatable;
use Gedmo\Mapping\Annotation as Gedmo;
use Sonata\TranslationBundle\Model\Gedmo\TranslatableInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Sonata\TranslationBundle\Model\Gedmo\AbstractPersonalTranslation;
use Sonata\TranslationBundle\Traits\Gedmo\PersonalTranslatableTrait;
/**
* BlogPost
*
* #ORM\Table(name="blog_post")
* #ORM\Entity(repositoryClass="AppBundle\Repository\BlogPostRepository")
* #Gedmo\TranslationEntity(class="AppBundle\Entity\Translations\BlogPostTr")
* #ORM\HasLifecycleCallbacks
*/
class BlogPost implements TranslatableInterface
{
use PersonalTranslatableTrait;
/**
* Post locale
* Used locale to override Translation listener's locale
*
* #Gedmo\Locale
*/
protected $locale;
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="blogPosts")
*/
private $category;
public function setCategory(Category $category)
{
$this->category = $category;
}
public function getCategory()
{
return $this->category;
}
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255)
* #Gedmo\Translatable
*/
private $title;
/**
* #var string
*
* #ORM\Column(name="body", type="text")
* #Gedmo\Translatable
*/
private $body;
/**
* #var bool
*
* #ORM\Column(name="draft", type="boolean")
*/
private $draft = false;
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set title
*
* #param string $title
*
* #return BlogPost
*/
public function setTitle($title)
{
$this->title = $title;
return $this;
}
/**
* Get title
*
* #return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Set body
*
* #param string $body
*
* #return BlogPost
*/
public function setBody($body)
{
$this->body = $body;
return $this;
}
/**
* Get body
*
* #return string
*/
public function getBody()
{
return $this->body;
}
/**
* Set draft
*
* #param boolean $draft
*
* #return BlogPost
*/
public function setDraft($draft)
{
$this->draft = $draft;
return $this;
}
/**
* Get draft
*
* #return bool
*/
public function getDraft()
{
return $this->draft;
}
// TRANSLATION
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Translations\BlogPostTr", mappedBy="object", cascade={"persist", "remove"})
*/
protected $translations;
public function __construct()
{
$this->translations = new ArrayCollection;
}
public function getTranslations()
{
return $this->translations;
}
public function addTranslation(AbstractPersonalTranslation $t)
{
$this->translations->add($t);
$t->setObject($this);
}
public function removeTranslation(AbstractPersonalTranslation $t)
{
$this->translations->removeElement($t);
}
public function setTranslations($translations)
{
$this->translations = $translations;
}
/**
* Sets translatable locale
*
* #param string $locale
*/
public function setTranslatableLocale($locale)
{
$this->locale = $locale;
}
}
BlogPostTr.php
<?php
namespace AppBundle\Entity\Translations;
use Doctrine\ORM\Mapping as ORM;
use Sonata\TranslationBundle\Model\Gedmo\AbstractPersonalTranslation;
/**
* #ORM\Entity
* #ORM\Table(name="blog_post_translation",
* uniqueConstraints={#ORM\UniqueConstraint(name="lookup_unique_idx", columns={
* "locale", "object_id", "field"
* })}
* )
*/
class BlogPostTr extends AbstractPersonalTranslation
{
/**
* Convinient constructor
*
* #param string $locale
* #param string $field
* #param string $content
*/
public function __construct($locale = null, $field = null, $content = null)
{
$this->setLocale($locale);
$this->setField($field);
$this->setContent($content);
}
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\BlogPost", inversedBy="translations")
* #ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $object;
}
BlogPostAdmin.php
<?php
namespace AppBundle\Admin;
use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
class BlogPostAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->tab('Post')
->with('Content', array('class' => 'col-md-9'))
->add('title', 'text')
// ->add('title', 'translatable_field', array(
// 'allow_extra_fields' => true,
// 'field' => 'title',
// 'personal_translation' => 'AppBundle\Entity\Translations\BlogPostTr',
// 'property_path' => 'translations',
// ))
->add('body', 'textarea')
->end()
->end()
->tab('Publishing options')
->with('Meta data', array('class' => 'col-md-3'))
->add('category', 'sonata_type_model', array(
'class' => 'AppBundle\Entity\Category',
'property' => 'name',
))
->end()
->end();
}
// protected function configureDatagridFilters(DatagridMapper $datagridMapper)
// {
// $datagridMapper->add('title');
// }
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->addIdentifier('title');
}
public function toString($object)
{
return $object instanceof BlogPost
? $object->getTitle()
: 'Blog Post'; // shown in the breadcrumb on the create view
}
}
Please help!
Had same problem. Actually I had two problems:
Entities weren't implementing Sonata\TranslationBundle\Model\Gedmo\TranslatableInterface. And even if its added later, symfony cache needs to be cleared.
I found that during Sonata Admin installation or one of sonata bundles I've added DoctrineExtensionListener like shown here. Inside that listener Gedmo initiates current locale and of course it doesn't know anything about SonataTranslateBundle.
So I would recommend to dump(debug) symfony locale from request parameters, sonata translation locale and gedmo initiated locale and check that these are synced.
In the end I've setup locale to be stored in user session and updated DoctrineExtensionListener to use locale from session.
Symfony version: 3.0
In my entity has a field to store the uploaded file path. I need to custom the upload field template, so i made a custom field type for the file field.
Entity/Store.php:
/**
* #var string
*/
private $thumbnail;
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* #var File
*/
private $thumbnailFile;
/**
* Set thumbnail
*
* #param string $thumbnail
*
* #return Store
*/
public function setThumbnail($thumbnail)
{
$this->thumbnail = $thumbnail;
return $this;
}
/**
* Get thumbnail
*
* #return string
*/
public function getThumbnail()
{
return $this->thumbnail;
}
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* #param File|\Symfony\Component\HttpFoundation\File\UploadedFile $file
*
* #return Store
*/
public function setThumbnailFile(UploadedFile $file = null)
{
$this->thumbnailFile = $file;
if ($file) {
// It is required that at least one field changes if you are using doctrine
// otherwise the event listeners won't be called and the file is lost
$this->updatedAt = new \DateTime('now');
}
return $this;
}
/**
* #return File
*/
public function getThumbnailFile()
{
return $this->thumbnailFile;
}
StoreType.php:
->add('thumbnailFile', SingleThumbnailType::class, array(
'required' => false,
'mapped' => false
))
SingleThumbnailType.php:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SingleThumbnailType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('singleThumbnail', FileType::class);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$parentData = $form->getParent()->getData();
$uploadedeUrl = '/uploads/';
$view->vars['thumbnail_url'] = $uploadedeUrl . 'product-default.jpg';
if($parentData->getThumbnail()) {
$view->vars['thumbnail_url'] = $uploadedeUrl . $parentData->getThumbnail();
}
}
}
When i selected the image and submitted, i got the error message:
Uncaught PHP Exception Symfony\Component\PropertyAccess\Exception\InvalidArgumentException: "Expected argument of type "Symfony\Component\HttpFoundation\File\File", "array" given" at /vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php line 254 {"exception":"[object] (Symfony\\Component\\PropertyAccess\\Exception\\InvalidArgumentException(code: 0): Expected argument of type \"Symfony\\Component\\HttpFoundation\\File\\File\", \"array\" given at /vendor/symfony/symfony/src/Symfony/Component/PropertyAccess/PropertyAccessor.php:254)"} []
I debug the $request data, the thumbnailFile is null in the store object data.
I use dump($form->getErrors(true)); got this message below:
FormErrorIterator {#948 ▼
-form: Form {#591 ▶}
-errors: array:1 [▼
0 => FormError {#936 ▼
-message: "This value is not valid."
#messageTemplate: "This value is not valid."
#messageParameters: array:1 [▼
"{{ value }}" => "object"
]
#messagePluralization: null
-cause: ConstraintViolation {#925 ▶}
-origin: Form {#609 ▼
-config: FormBuilder {#610 ▶}
-parent: Form {#591}
-children: OrderedHashMap {#611 ▶}
-errors: []
-submitted: true
-clickedButton: null
-modelData: null
-normData: null
-viewData: UploadedFile {#22 ▶}
-extraData: []
-transformationFailure: TransformationFailedException {#818 ▶}
-defaultDataSet: true
-lockSetData: false
}
}
]
}
But if i use the filetype directory on parent form type(StoreType.php not SingleThumbnailType.php), i can get the thumbnail data.
->add('thumbnailFile', FileType::class, array(
'required' => false,
))
Add to 'thumbnailFile' FileType::class,array('data_class' => null)
Working with PHPUnit and Doctrine I frequently end up writing very large methods for mocking Doctrines ClassMetadata, although in my opinion it does not need to be mocked, because it can be seen as stable. Still I need to mock the EntityManager because I don't want Doctrine to connect to a database.
So here's my question: How do I get my ClassMetadata via the EntityManager mock without needing a database connection? For all eventual database calls the EntityManager still needs to be a mock, I just don't want to write all my metadata down again.
I'm using the DoctrineModule for Zend 2 so it would be useful to be able to use my configuration to get the Metadata object, but I assume it's also ok to read the required sections manually.
Example:
public function testGetUniqueFields()
{
$this->prepareGetUniqueFields(); // about 50 lines of mocking ClassMetadata
$entity = 'UniqueWithoutAssociation';
$unique = $this->handler->getUniqueFields($entity);
$expected = ["uniqueColumn"];
$this->assertEquals($expected, $unique,
'getUniqueFields does not return the unique fields');
}
And the code of the actual class:
public function getUniqueFields($class)
{
$unique = array();
$metadata = $this->getClassMetadata($class);
$fields = $metadata->getFieldNames();
foreach ($fields as $field) {
if($metadata->isUniqueField($field) && !$metadata->isIdentifier($field)) {
$unique[] = $field;
}
}
return $unique;
}
The test works like expected, but every time I test another method or another behavior of the method, I need to prepare the mocks again or combine past definitions. Plus, the 50 lines I need for this code are the least I have in this test. Most of the test class is all about the ClassMetadata mock. It is a time consuming and - if you see ClassMetadata as a stable component - unnecessary work.
After spending many hours starring at the Doctrine source code, I found a solution.
Once again, this solution is only if you are working with Doctrines ClassMetadata object so often that it becomes unclean to mock every method call. In every other case you should still create a mock of ClassMetadata.
Still, since the composers minimum stability setting was set to stable, such components can be seen as stable so there is no absolute need to create a mock object.
ClassMetadata depends on several other Doctrine classes, which are all injected through the ubiquitous EntityManager:
Doctrine\ORM\Configuration to get the entity path
Doctrine\Common\Annotations\AnnotationReader and Doctrine\ORM\Mapping\Driver\AnnotationDriver injected through the Configuration object
Doctrine\DBAL\Connection to get the database platform in order to know about the identifier strategy. This object should be mocked so no database calls are possible
Doctrine\DBAL\Platforms\AbstractPlatform as mentioned
Doctrine\Common\EventManager to trigger some events
For single test methods or simple method calls I created a method returning an EntityManager mock object that is capable of returning a valid ClassMetadata object:
/**
* #return EntityManager|\PHPUnit_Framework_MockObject_MockObject
*/
public function getEmMock()
{
$dir = __DIR__."/Asset/Entity/";
$config = Setup::createAnnotationMetadataConfiguration(array($dir), true);
$eventManager = new \Doctrine\Common\EventManager();
$platform = new PostgreSqlPlatform();
$metadataFactory = new ClassMetadataFactory();
$config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader()));
$connectionMock = $this->getMockBuilder('Doctrine\DBAL\Connection')
->disableOriginalConstructor()
->getMock();
$connectionMock->expects($this->any())
->method('getDatabasePlatform')
->will($this->returnValue($platform));
/** #var EntityManager|\PHPUnit_Framework_MockObject_MockObject $emMock */
$emMock = $this->getMockBuilder('Doctrine\ORM\EntityManager')
->disableOriginalConstructor()
->getMock();
$metadataFactory->setEntityManager($emMock);
$emMock->expects($this->any())
->method('getConfiguration')
->will($this->returnValue($config));
$emMock->expects($this->any())
->method('getConnection')
->will($this->returnValue($connectionMock));
$emMock->expects($this->any())
->method('getEventManager')
->will($this->returnValue($eventManager));
$emMock->expects($this->any())
->method('getClassMetadata')
->will($this->returnCallback(function($class) use ($metadataFactory){
return $metadataFactory->getMetadataFor($class);
}));
return $emMock;
}
Here you could even manipulate all objects by calling their getters created for the EntityManager mock. But that wouldn't be exactly clean and the method remains inflexible in some cases. Still a simple solution and you could e.g. add some parameters and put the method in a trait to reuse it.
For further needs, I created an abstract class that offers a maximum of flexibility and allows you to mock everything else or create some components in a whole other way.
It needs two configurations: The entity path and the platform object. You can manipulate or replace any object by setting it in the setUp method and then get the required EntityManager mock with getEmMock().
A little bit larger, but here it is:
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Tools\Setup;
/**
* Class AbstractTestWithMetadata
* #author Marius Teller
*/
abstract class AbstractTestWithMetadata extends \PHPUnit_Framework_TestCase
{
const EXCEPTION_NO_ENTITY_PATHS_SET = "At least one entity path must be set";
const EXCEPTION_NO_PLATFORM_SET = "An instance of Doctrine\\DBAL\\Platforms\\AbstractPlatform must be set";
/**
* #var array
*/
protected $entityPaths = [];
/**
* #var AbstractPlatform
*/
protected $platform;
/**
* #var EntityManager
*/
protected $emMock;
/**
* #var Connection
*/
protected $connectionMock;
/**
* #var Configuration
*/
protected $configuration;
/**
* #var EventManager
*/
protected $eventManager;
/**
* #var ClassMetadataFactory
*/
protected $classMetadataFactory;
/**
* #return array
* #throws \Exception
*/
public function getEntityPaths()
{
if($this->entityPaths === []) {
throw new \Exception(self::EXCEPTION_NO_ENTITY_PATHS_SET);
}
return $this->entityPaths;
}
/**
* #param array $entityPaths
*/
public function setEntityPaths(array $entityPaths)
{
$this->entityPaths = $entityPaths;
}
/**
* add an entity path
* #param string $path
*/
public function addEntityPath($path)
{
$this->entityPaths[] = $path;
}
/**
* #return AbstractPlatform
* #throws \Exception
*/
public function getPlatform()
{
if(!isset($this->platform)) {
throw new \Exception(self::EXCEPTION_NO_PLATFORM_SET);
}
return $this->platform;
}
/**
* #param AbstractPlatform $platform
*/
public function setPlatform(AbstractPlatform $platform)
{
$this->platform = $platform;
}
/**
* #return EntityManager
*/
public function getEmMock()
{
if(!isset($this->emMock)) {
/** #var EntityManager|\PHPUnit_Framework_MockObject_MockObject $emMock */
$emMock = $this->getMockBuilder('Doctrine\ORM\EntityManager')
->disableOriginalConstructor()
->getMock();
$config = $this->getConfiguration();
$connectionMock = $this->getConnectionMock();
$eventManager = $this->getEventManager();
$classMetadataFactory = $this->getClassMetadataFactory();
$classMetadataFactory->setEntityManager($emMock);
$emMock->expects($this->any())
->method('getConfiguration')
->will($this->returnValue($config));
$emMock->expects($this->any())
->method('getConnection')
->will($this->returnValue($connectionMock));
$emMock->expects($this->any())
->method('getEventManager')
->will($this->returnValue($eventManager));
$emMock->expects($this->any())
->method('getClassMetadata')
->will($this->returnCallback(function($class) use ($classMetadataFactory){
return $classMetadataFactory->getMetadataFor($class);
}));
$this->setEmMock($emMock);
}
return $this->emMock;
}
/**
* #param EntityManager $emMock
*/
public function setEmMock($emMock)
{
$this->emMock = $emMock;
}
/**
* #return Connection
*/
public function getConnectionMock()
{
if(!isset($this->connectionMock)) {
$platform = $this->getPlatform();
/** #var Connection|\PHPUnit_Framework_MockObject_MockObject $connectionMock */
$connectionMock = $this->getMockBuilder('Doctrine\DBAL\Connection')
->disableOriginalConstructor()
->getMock();
$connectionMock->expects($this->any())
->method('getDatabasePlatform')
->will($this->returnValue($platform));
$this->setConnectionMock($connectionMock);
}
return $this->connectionMock;
}
/**
* #param Connection $connectionMock
*/
public function setConnectionMock($connectionMock)
{
$this->connectionMock = $connectionMock;
}
/**
* #return Configuration
*/
public function getConfiguration()
{
if(!isset($this->configuration)) {
$config = Setup::createAnnotationMetadataConfiguration($this->getEntityPaths(), true);
$config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader()));
$this->setConfiguration($config);
}
return $this->configuration;
}
/**
* #param Configuration $configuration
*/
public function setConfiguration(Configuration $configuration)
{
$this->configuration = $configuration;
}
/**
* #return EventManager
*/
public function getEventManager()
{
if(!isset($this->eventManager)) {
$this->setEventManager(new EventManager());
}
return $this->eventManager;
}
/**
* #param EventManager $eventManager
*/
public function setEventManager($eventManager)
{
$this->eventManager = $eventManager;
}
/**
* #return ClassMetadataFactory
*/
public function getClassMetadataFactory()
{
if(!isset($this->classMetadataFactory)) {
$this->setClassMetadataFactory(new ClassMetadataFactory());
}
return $this->classMetadataFactory;
}
/**
* #param ClassMetadataFactory $classMetadataFactory
*/
public function setClassMetadataFactory(ClassMetadataFactory $classMetadataFactory)
{
$this->classMetadataFactory = $classMetadataFactory;
}
}
One more hint: You might have problems with annotations of other classes, e.g. Zend\Form\Annotation\Validator. Such annotations will throw an exception in Doctrines parser because this parser does not use auto loading and only checks for already loaded classes. So if you still want to use them, you just have to include them manually before parsing the classes annotations.
Problem
Hello, I am using Symfony CMF 1.2, liip/imagine-bundle 1.3, symfony-cmf/media-bundle 1.2. I want to add 2 additional image fields to my block that extends ImagineBlock because for every image I upload there will be a mobile and tablet version of the image which is not a simple resize, the aspect ratio or whatnot is not similar. I cannot just crop/resize without affecting the quality of the image.
Attempts
My block
namespace xx\BlockBundle\Document;
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;
use Symfony\Cmf\Bundle\BlockBundle\Doctrine\Phpcr\ImagineBlock;
use Symfony\Cmf\Bundle\MediaBundle\Doctrine\Phpcr\Image;
use Symfony\Cmf\Bundle\MediaBundle\ImageInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Class ClickableBlock
* #package xx\BlockBundle\Document
* #PHPCR\Document(referenceable=true)
*/
class ClickableBlock extends ImagineBlock
{
/**
* #PHPCR\Child(nodeName="image-mobile", cascade={"persist"})
* #var Image
*/
protected $imageMobile;
/**
* #PHPCR\Child(nodeName="image-tablet", cascade={"persist"})
* #var Image
*/
protected $imageTablet;
public function setIsPublishable($publishable)
{
$this->setPublishable($publishable);
}
/**
* #return Image
*/
public function getImageMobile()
{
return $this->imageMobile;
}
/**
* #return Image
*/
public function getImageTablet()
{
return $this->imageTablet;
}
/**
* Set the imageMobile for this block.
*
* #param ImageInterface|UploadedFile|null $image optional the imageMobile to update
* #return $this
* #throws \InvalidArgumentException If the $image parameter can not be handled.
*/
public function setImageMobile($image = null)
{
return $this->processImage($image, 'image-mobile', $this->imageMobile);
}
/**
* Set the imageTablet for this block.
*
* #param ImageInterface|UploadedFile|null $image optional the imageTablet to update
* #return $this
* #throws \InvalidArgumentException If the $image parameter can not be handled.
*/
public function setImageTablet($image = null)
{
return $this->processImage($image, 'image-tablet', $this->imageTablet);
}
/**
* #param ImageInterface|UploadedFile|null $image
* #param string $imageName
* #param Image $imageRef
* #return $this
*/
protected function processImage($image, $imageName, $imageRef)
{
if (!$image) {
return $this;
}
if (!$image instanceof ImageInterface && !$image instanceof UploadedFile) {
$type = is_object($image) ? get_class($image) : gettype($image);
throw new \InvalidArgumentException(sprintf(
'Image is not a valid type, "%s" given.',
$type
));
}
if ($imageRef) {
// existing imageTablet, only update content
$imageRef->copyContentFromFile($image);
} elseif ($image instanceof ImageInterface) {
$image->setName($imageName); // ensure document has right name
$imageRef = $image;
} else {
$imageRef = new Image();
$imageRef->copyContentFromFile($image);
}
return $this;
}
}
Admin:
namespace xx\BlockBundle\Admin;
use xx\BlockBundle\Document\ClickableBlock;
use xx\MainBundle\Form\Common\FormMapper as CommonFormMapper;
use Cocur\Slugify\Slugify;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Symfony\Cmf\Bundle\BlockBundle\Admin\Imagine\ImagineBlockAdmin;
class ClickableBlockAdmin extends ImagineBlockAdmin
{
/**
* {#inheritdoc}
*/
public function toString($object)
{
return $object instanceof ClickableBlock && $object->getLabel()
? $object->getLabel()
: parent::toString($object);
}
/**
* {#inheritdoc}
*/
public function prePersist($document)
{
parent::prePersist($document);
$this->InitialiseDocument($document);
}
/**
* #param $document
*/
private function InitialiseDocument(&$document)
{
$documentManager = $this->getModelManager();
$parentDocument = $documentManager->find(null, '/cms/xx/block');
$document->setParentDocument($parentDocument);
$slugifier = new Slugify();
$document->setName($slugifier->slugify($document->getLabel()));
}
/**
* {#inheritdoc}
*/
public function preUpdate($document)
{
parent::preUpdate($document);
$this->InitialiseDocument($document);
}
/**
* {#inheritdoc}
*/
protected function configureFormFields(FormMapper $formMapper)
{
parent::configureFormFields($formMapper);
if (null === $this->getParentFieldDescription()) {
$imageRequired = ($this->getSubject() && $this->getSubject()->getParentDocument()) ? false : true;
$formMapper
->with('form.group_general')
->remove('parentDocument')
->remove('filter')
->add('parentDocument', 'hidden', ['required' => false, 'data' => 'filler'])
->add('name', 'hidden', ['required' => false, 'data' => 'filler'])
->add('imageMobile', 'cmf_media_image', array('required' => $imageRequired))
->add('imageTablet', 'cmf_media_image', array('required' => $imageRequired))
->end();
// Append common fields to FormMapper
$commonFormMapper = new CommonFormMapper($formMapper);
$formMapper = $commonFormMapper->getPublishingFields();
}
}
}
Note I am unable to inject service container to this class (via constructor/method), that is why am using hardcoded node path and instantiated Slugify class instead of using it's service for now. I am all ears for a solution to this also. Ref -
xx.main.admin.pageadmin.container:
class: xx\MainBundle\Admin\PageAdmin
calls:
- [setContainer,[ #service_container ]]
# arguments: ["#service_container"]
The annotations on the image fields are based on the following config I found in
\vendor\symfony-cmf\block-bundle\Resources\config\doctrine-phpcr\ImagineBlock.phpcr.xml:
<doctrine-mapping
xmlns="http://doctrine-project.org/schemas/phpcr-odm/phpcr-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/phpcr-odm/phpcr-mapping
https://github.com/doctrine/phpcr-odm/raw/master/doctrine-phpcr-odm-mapping.xsd"
>
<document
name="Symfony\Cmf\Bundle\BlockBundle\Doctrine\Phpcr\ImagineBlock"
referenceable="true"
translator="attribute"
>
<node name="node"/>
<locale name="locale"/>
<field name="label" type="string" translated="true" nullable="true"/>
<field name="linkUrl" type="string" translated="true" nullable="true"/>
<field name="filter" type="string" nullable="true"/>
<child name="image" node-name="image">
<cascade>
<cascade-persist/>
</cascade>
</child>
</document>
</doctrine-mapping>
Result
While the default "image" field persists normally, the other two added image fields are not taken into consideration since when I debug on prePersist I see that both fields are null while image field contains its uploaded file.
I tried adding a normal text field which saved and displayed normally on my page.
I use YAML in my project, so I am not sure how exactly the given XML translates, if ever it is the correct mapping to define.
Please help. :)
A colleague found the issue which was the following:
protected function processImage($image, $imageName, $imageRef)
should be
protected function processImage($image, $imageName, &$imageRef)
$imageRef was not passed by reference making it always null. Silly me. Let's hope this code at least helps other people. :)
For the admin question: phpcr-odm admins have a rootPath for exactly the purpose of what you are doing. you could add to your service definition like this:
<call method="setRootPath">
<argument>%cmf_content.persistence.phpcr.content_basepath%</argument>
</call>
and then you do $this->getRootPath()