Sonata Admin entities translation on Symfony 3 - translation

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.

Related

Discord oAuth2 login after authorization was given

I have a symfony4 application and I'm using knpuniversity/oauth2-client-bundle to authenticate a user against discords oAuth endpoint.
The user clicks on the 'Login via Discord' button, he sees the authorization page from discord, accepts it and is now logged in on my page.
So far so good.
Here is what is not working:
The user is on another computer, so no session on my page. He logs in to discord web client. After that he visits my page but he is not logged in.
When he clicks again on 'Login via Discord' he sees the authorization page again instead of directly being logged in on my page.
Maybe I get something wrong here but usually when I use an oAuth login with google, facebook or anything else just like here on stackoverflow I never see the authorization page again. I click on 'login with XY' and, as long as I'm logged in to my corresponding account, I will immediately be logged in on the other page as well.
<?php
namespace App\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2Client;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use League\OAuth2\Client\Token\AccessToken;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class DiscordAuthenticator extends SocialAuthenticator
{
/**
* #var \KnpU\OAuth2ClientBundle\Client\ClientRegistry
*/
private $clientRegistry;
/**
* #var \App\Repository\UserRepository
*/
private $repository;
/**
* #var \Symfony\Component\Routing\RouterInterface
*/
private $router;
/**
* DiscordAuthenticator constructor.
*
* #param \KnpU\OAuth2ClientBundle\Client\ClientRegistry $clientRegistry
* #param \App\Repository\UserRepository $repository
* #param \Symfony\Component\Routing\RouterInterface $router
*/
public function __construct(
ClientRegistry $clientRegistry,
UserRepository $repository,
RouterInterface $router
) {
$this->clientRegistry = $clientRegistry;
$this->repository = $repository;
$this->router = $router;
}
/**
* #inheritDoc
*/
public function start(Request $request, AuthenticationException $authException = null)
{
return new RedirectResponse('/login/', Response::HTTP_TEMPORARY_REDIRECT);
}
/**
* #inheritDoc
*/
public function supports(Request $request): bool
{
return $request->attributes->get('_route') === 'discord_set_token';
}
/**
* #inheritDoc
*/
public function getCredentials(Request $request)
{
return $this->fetchAccessToken($this->getDiscordClient());
}
/**
* #inheritDoc
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
try {
/** #var \Wohali\OAuth2\Client\Provider\DiscordResourceOwner $discordUser */
$discordUser = $this->getDiscordClient()
->fetchUserFromToken($credentials);
$email = $discordUser->getEmail();
$existingUser = $this->repository->findOneBy(
[
'externalId' => $discordUser->getId(),
'externalIdSource' => 'discord',
]
);
if ($existingUser) {
return $existingUser;
}
$user = $this->repository->findOneBy(['email' => $email]);
if (!$user) {
$user = new User(
$discordUser->getId(),
'discord',
$discordUser->getUsername(),
$discordUser->getEmail()
);
$user->setExternalId($discordUser->getId());
$user->setExternalIdSource('discord');
$user->setToken($credentials->getToken());
$user->setRefreshToken($credentials->getRefreshToken());
$user->setTokenExpiresFromTimestamp($credentials->getExpires());
}
$this->repository->persist($user);
return $user;
} catch (\Throwable $e) {
throw new AuthenticationException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* #inheritDoc
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_FORBIDDEN);
}
/**
* #inheritDoc
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): Response
{
$targetUrl = $this->router->generate('home');
return new RedirectResponse($targetUrl);
}
/**
* #return \KnpU\OAuth2ClientBundle\Client\OAuth2Client
*/
private function getDiscordClient(): OAuth2Client
{
return $this->clientRegistry
->getClient('discord');
}
}
<?php
namespace App\Security;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class UserProvider implements UserProviderInterface
{
/**
* #var \App\Repository\UserRepository
*/
private $repository;
/**
* UserProvider constructor.
*
* #param \App\Repository\UserRepository $repository
*/
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
/**
* #inheritDoc
*/
public function loadUserByUsername($username): UserInterface
{
$user = $this->repository->findOneBy(['email' => $username]);
if (!$user) {
throw new UsernameNotFoundException(sprintf('No User with username %s found', $username));
}
return $user;
}
/**
* #inheritDoc
*/
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
// TODO find out how to use the refresh token to get a new one
return $user;
}
/**
* #inheritDoc
*/
public function supportsClass($class): bool
{
return $class === User::class;
}
}
security:
providers:
user_provider:
entity:
class: App:User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
logout:
path: /logout
target: /login
guard:
authenticators:
- App\Security\DiscordAuthenticator
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManager;
/**
* Class UserRepository
*/
class UserRepository
{
/**
* #var \Doctrine\ORM\EntityManager
*/
private $entityManager;
/**
* #var \Doctrine\ORM\EntityRepository
*/
private $repository;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
$this->repository = $entityManager->getRepository(User::class);
}
/**
* Finds an entity by its primary key / identifier.
*
* #param int $id
*
* #return \App\Entity\User|null
*/
public function find(int $id): ?User
{
return $this->repository->find($id);
}
/**
* Finds all entities in the repository.
*
* #return \App\Entity\User[]
*/
public function findAll(): iterable
{
return $this->repository->findAll();
}
/**
* Finds entities by a set of criteria.
*
* #param array $criteria
* #param array|null $orderBy
* #param int|null $limit
* #param int|null $offset
*
* #return \App\Entity\User[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): iterable
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
/**
* Finds a single entity by a set of criteria.
*
* #param array $criteria
* #param array|null $orderBy
*
* #return \App\Entity\User|null
*/
public function findOneBy(array $criteria, array $orderBy = null): ?User
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* Counts entities by a set of criteria.
*
* #param array $criteria
*
* #return int
*/
public function count(array $criteria): int
{
return $this->repository->count($criteria);
}
/**
* Select all elements from a selectable that match the expression and
* return a new collection containing these elements.
*
* #param \Doctrine\Common\Collections\Criteria $criteria
*
* #return \App\Entity\User[]
*/
public function matching(Criteria $criteria): iterable
{
return $this->repository->matching($criteria);
}
/**
* #param \App\Entity\User $user
*
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
*/
public function persist(User $user): void
{
$this->entityManager->merge($user);
$this->entityManager->flush();
}
}
<?php
namespace App\Controller;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* #Route(path="discord/")
*/
class DiscordOAuthController extends AbstractController
{
/**
* #var \KnpU\OAuth2ClientBundle\Client\ClientRegistry
*/
private $clientRegistry;
/**
* DiscordOAuthController constructor.
*
* #param \KnpU\OAuth2ClientBundle\Client\ClientRegistry $clientRegistry
*/
public function __construct(ClientRegistry $clientRegistry)
{
$this->clientRegistry = $clientRegistry;
}
/**
* #Route(name="discord_redirect_authorization", path="redirect_authorization")
*
* #return \Symfony\Component\HttpFoundation\Response
*/
public function redirectAuthorizationAction(): Response
{
return $this->clientRegistry->getClient('discord')
->redirect(['identify', 'email', 'guilds']);
}
/**
* #Route(name="discord_set_token", path="set_token")
*
* #return void
*/
public function setTokenAction(): void
{
// empty as authenticator will handle the request
}
}
At the time I was working on this I didn't had much time and only briefly looked into the documentation.
Now I have had more time and put some time into reading the whole documentation of Discords oAuth.
I then found the query param prompt which when set to none will only once request the authorisation and otherwise will directly redirect to the redirect_uri

How to access properties of node over 2 relations with NEO4J-PHP-OGM

I cant get my head around how to access properties over 2 relationships with the neo4j-php-ogm library.
Say for example I have a "user" node, which connects to MANY "resource" nodes, which in return are each connected to a fixed number of predefined "MetaResource" node. The "resource" nodes have properties and the "MetaResource" nodes have the Meta-Properties of each resource type. How can I know access the properties of the "MetaResource" nodes starting from a "user" node? In Neo4j one such route looks like this:
(user)-[r:HAS_RESOURCE]->(resource)-[m:METARESOURCE]->(metaResource)
My example user entity:
/**
* #OGM\Node(label="User")
*/
class User
{
/**
* #OGM\GraphId()
* #var int
*/
private $id;
/**
* #OGM\Relationship(type="HAS_RESOURCE", direction="OUTGOING", targetEntity="\AppBundle\Entity\Resources", collection=true)
* #var ArrayCollection|\AppBundle\Entity\Resources[]
*/
protected $resources;
/**
* #return \Doctrine\Common\Collections\ArrayCollection|\AppBundle\Entity\Resources[]
*/
public function getResources()
{
return $this->resources;
}
/**
* #return \Doctrine\Common\Collections\ArrayCollection|\AppBundle\Entity\Resources[]
*/
public function getResource($name)
{
foreach($this->resources as $resource){
if($resource->getResourceType() == $name){
return $resource;
break;
}
}
}
/**
* #param AppBundle\Entity\Resources $resources
*/
public function addResource(Resources $resources)
{
if (!$this->resources->contains($resources)) {
$this->resources->add($resources);
}
}
}
My Example Resource Entity:
/**
* #OGM\Node(label="Resources")
*/
class Resources
{
/**
* #OGM\GraphId()
* #var int
*/
protected $id;
/**
* #OGM\Property(type="int")
* #var int
*/
protected $resourcecount;
/**
* #OGM\Relationship(type="METARESOURCE", direction="OUTGOING", targetEntity="\AppBundle\Entity\MetaResource", collection=false)
* #var MetaResource
*/
protected $metaResource;
/**
* #param \AppBundle\Entity\MetaResource $metaResource
*/
public function __construct(MetaResource $metaResource)
{
$this->metaResource = $metaResource;
}
public function getId()
{
return $this->id;
}
public function getResourceCount()
{
return $this->resourcecount;
}
public function setResourceCount($resourcecount)
{
$this->resourcecount = $resourcecount;
}
/**
* #return \AppBundle\Entity\MetaResource
*/
public function getMetaResource()
{
return $this->metaResource;
}
}
And my Example MetaResource Entity:
/**
* #OGM\Node(label="MetaResource")
*/
class MetaResource
{
/**
* #OGM\GraphId()
* #var int
*/
protected $id;
/**
* #OGM\Property(type="string")
* #var string
*/
protected $resourceType;
/**
* #OGM\Property(type="string")
* #var string
*/
protected $name_DE;
/**
* #OGM\Property(type="string")
* #var string
*/
protected $icon;
/**
* #OGM\Property(type="string")
* #var string
*/
protected $iconColour;
/**
* #OGM\Property(type="string")
* #var string
*/
protected $colour;
public function getResourceType()
{
return $this->resourceType;
}
public function getName_DE()
{
return $this->name_DE;
}
public function getIcon()
{
return $this->icon;
}
public function getIconColour()
{
return $this->iconColour;
}
public function getColour()
{
return $this->colour;
}
}
And finally the code from my controller, which sets up the relations:
$metaResource=$em->getRepository(MetaResource::class)->findOneBy('resourceType', 'wood');
$rWood = new Resources($metaResource);
$rWood->setResourceCount(20);
$em->persist($rWood);
$em->flush();
$user->addResource($rWood);
If I now have a look at the Neo4j webconsole, all relationships and nodes are correctly inserted.
Now, if I get the resources of a user with $user->getResources() I successfully get all the resource objects, but the "$metaResource" property is always NULL instead of the anticipated Object of my MetaResource entity. For example if I do:
foreach($user->getResources() as $resource){
var_dump($resource->getMetaResource());
}
Then it outputs only NULLs.
On the other hand, if I directly get a resource object (for example with $resource = $em->getRepository(Resources::class)->findOneById(123) ) and then try to get the connected MetaResource with $resource->getMetaResource() it works. What am I missing?
Cheers
I have made some progress regarding this use case. I'm using now a proxy generator that can handle this use case (this was a big missing part into the lib actually but takes time to implement).
So please test with the latest release 1.0.0-beta7.

Symfongy Custom Form Type: Why i can not get the uploaded data?

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)

Implement ModuleManagerInterface

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__,
],
],
];
}
}

Symfony CMF multiple image fields using ImagineBlock

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()

Resources