Symfony form, One-to-one relationship, one side can be null - doctrine-orm

I have a MainConfig entity that has One-to-one relationship with LedConfig entity. LedConfig can be null.
<?php
namespace ...
use Doctrine\ORM\Mapping as ORM;
...
/**
* #ORM\Entity
*/
class MainConfig
{
...
/**
* #ORM\OneToOne(targetEntity="...\LedConfig", cascade={"all"})
* #ORM\JoinColumn(name="led_config_id", referencedColumnName="id", nullable=true, unique = true)
*/
private $ledConfig = null;
...
}
<?php
namespace ...
use Doctrine\ORM\Mapping as ORM;
...
/**
* #ORM\Entity
*/
class LedConfig
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="float")
* #Assert\Type(type="float")
*/
private $lowerVoltageThreshold = 11.9;
/**
* #ORM\Column(type="float")
* #Assert\Type(type="float")
*/
private $upperVoltageThreshold = 12.85;
}
<?php
namespace ...;
use Symfony\Component\Form\AbstractType;
...
class MainConfigType extends AbstractType
{
...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(...)
->add('ledConfig', LedConfigType::class)
...
}
...
}
I am inserting a main config into a database automatically without using Symfony forms. So, if I set led config to null in the code - field led_config_id inside main config table is correctly set to NULL.
But, when I am updating the main config, I am using HTTP PUT request that is processed through Symfony forms.
I am using JSON, so request body looks like this:
{..., "ssid":"test","runningMode":"force_on","led_config":null, ...}
After the form is processed, property $ledConfig inside MainConfig entity is magically instantiated as LedConfig object with all properties set to NULL?!
And database update fails because it tries to save LedEnttiy with empty fields.
Why does it happen?
Can someone please help me and indicate what I did wrong?
EDIT:
I could hack it in the controller update action, after the form validation:
/**
* #Route("...")
* #Method("PUT")
*/
public function updateMainConfigAction($id, Request $request)
{
$requestData = \GuzzleHttp\json_decode($request->getContent(), true);
...
if (!$form->isValid()) {
throw $this->throwApiProblemValidationException($form);
}
if (empty($requestData['led_config'])) {
$mainConfig->setLedConfig(null);
}
$em = $this->getDoctrine()->getManager();
$em->persist($mainConfig);
$em->flush();
...
}
but that is kind of dirty...

There are the empty_data and the required options on Symfony's FormType.
The empty_data option has following behaviour:
when data_class is set and required option is true, then empty_data is new $data_class();
when data_class is set and required option is false, then empty_data is null;
when data_class is not set and compound option is true, then empty_data is empty array;
when data_class is not set and compound option is false, then empty_data is empty string.
The required option is true by default and the data_class is set on the ledConfig field, so when you left it empty, then new class is set to it. If you want the ledConfig field to be explicitly set to null when no value is selected, you can set directly the empty_data option.
In your case, you want the #2 scenario, so set required option to false and empty_data option set to null on ledConfig field:
...
$builder
->add(...)
->add('ledConfig', LedConfigType::class, array(
'required' => false
'empty_data' => null
))
...

Related

How to use Entities Classes from Different Locations in CodeIgniter 4 using Doctrine 2

I am using CodeIgniter 4.x for my project. In my approach, I am using Module system for my project. My Module system is like below structure:
- App
- Modules
- ListMaster
- User
- Models
- Doctrine
- Entities
- User.php
- UserApp.php
- UserGroup
- Models
- Doctrine
- Entities
- Usergroup.php
- Budget
- Models
- Doctrine
- Entities
- Budget.php
In a simple word, I have designed to have my Modules under App/Modules folder. This Folder could consist a Module folder or a folder which will hold Modules. As I created two Modules App/Modules/ListMaster/User and App/Modules/ListMaster/Usergroup. My Entity Class Files will be located under ...Models/Doctrine/Entities folders.
I have downloaded Doctrine using Composer. My Composer is as follow:
{
"require": {
"doctrine/orm": "^2.3.3",
"doctrine/dbal": "^3.2",
"doctrine/annotations": "^1.14",
"symfony/yaml": "^5.4",
"symfony/cache": "^5.4"
},
"autoload": {
"psr-0": {"": "src/"}
},
}
I have created a Library file under App/Libraries which is as follow:
<?php
namespace App\Libraries;
//WE INCLUDING THE AUTOLOAD VENDOR
include_once dirname(__DIR__, 2) . '/vendor/autoload.php';
use Doctrine\Common\ClassLoader;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup;
// TO OBTAIN THE ENTITY MANAGER
class Doctrine
{
public $em;
public function __construct()
{
// Retrieving all paths leading to entities classes
$modulePath = APPPATH."Models/Doctrine/Entities";
$entitiesPath = glob($modulePath, GLOB_ONLYDIR);
$modulePath = APPPATH."Modules/*/Models/Doctrine/Entities";
$entitiesPath = array_merge($entitiesPath, glob($modulePath, GLOB_ONLYDIR));
$modulePath = APPPATH."Modules/*/*/Models/Doctrine/Entities";
$entitiesPath = array_merge($entitiesPath, glob($modulePath, GLOB_ONLYDIR));
$isDevMode = true;
$cache = null;
$useSimpleAnnotationReader = false;
// CONNECTION SETUP
$config = config("Database");
$connection_options = array(
'driver' => strtolower($config->doctrine['DBDriver']),
'user' => $config->doctrine['username'],
'password' => $config->doctrine['password'],
'dbname' => $config->doctrine['database'],
'host' => $config->doctrine['hostname']
);
$proxies_dir = APPPATH . 'Models/Doctrine/Proxies';
$config = Setup::createAnnotationMetadataConfiguration($entitiesPath, $isDevMode, $proxies_dir, $cache, $useSimpleAnnotationReader);
if (ENVIRONMENT === 'development') {
$config->setAutoGenerateProxyClasses(true);
} else {
$config->setAutoGenerateProxyClasses(false);
}
try {
$this->em = EntityManager::create($connection_options, $config);
} catch (\Doctrine\ORM\ORMException $e) {
log_message("Doctrine Exception : ", $e);
}
}
}
As you can see from this, My Entities files will be at Models/Doctrine/Entities, Modules/{any folder}/Models/Doctrine/Entities, Modules/{any folder}/{any folder}/Models/Doctrine/Entities folders.
Now here is my sample Entities Class:
<?php
namespace entities;
use Doctrine\ORM\Mapping as ORM;
/**
* User
*
* #ORM\Table(name="user")
* #ORM\Entity
*/
class User
{
/**
* #var smallint $id
*
* #ORM\Column(name="id", type="smallint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #var string $userName
*
* #ORM\Column(name="user_name", type="string", length=150, nullable=true)
*/
protected $userName;
/**
* Constructor
*/
public function __construct()
{
}
/**
* Get id
*
* #return boolean
*/
public function getId()
{
return $this->id;
}
/**
* Set userName
*
* #param string $userName
*/
public function setUserName($userName)
{
$this->userName = $userName;
}
/**
* Get userName
*
* #return string
*/
public function getUserName()
{
return $this->userName;
}
}
Now from my Controller I am executing this Line of code which produces entities\User class not found error. Though this structure can create and update Schema in Database successfully.
$users = $this->em->getRepository('entities\User')->findAll();
Please be noted that I have successfully Generate Proxy, Generate Entities, Crate Schema, Update Schema from this installation.
Thanks in Advance. Please let me know if I missed something to inform you.
So my question is, How to fix the class not found error and got result from Database from any table?

Symfony4 - Doctrine Cross Database joins configuration

I need cross database relations, i've read about this buti can't get what i want due to a mapping issue.
This is my situation
namespace App\Entity\Utility;
use Doctrine\ORM\Mapping as ORM;
use App\Entity\Crm\User;
/**
* Description of Test
*
* #ORM\Table(name="fgel_utility.fgel_test")
* #ORM\Entity(repositoryClass="App\Repository\Utility\TestRepository")
*/
class Test
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
*
* #var User
*
* #ORM\ManyToOne(targetEntity="App\Entity\Crm\User")
* #ORM\JoinColumn(name="user_cod", referencedColumnName="AUCUT")
*/
protected $user = null;
public function getId()
{
return $this->id;
}
public function getUser(): User
{
return $this->user;
}
public function setId($id)
{
$this->id = $id;
return $this;
}
public function setUser(User $user)
{
$this->user = $utente;
return $this;
}
}
namespace App\Entity\Crm;
use Doctrine\ORM\Mapping as ORM;
/**
*
* #ORM\Table(name="crm.USER")
* #ORM\Entity(repositoryClass="App\Repository\FintelGasDati\AnuteRepository")
*/
class User
{
/**
*
* #ORM\Id
* #ORM\Column(name="AUCUT", type="integer", nullable=false)
*/
protected $codiceCliente;
# SOME CODE
}
My doctrine.yaml
doctrine:
orm:
default_entity_manager: default
entity_managers:
#################################
# Update schema only with this em
#################################
default:
connection: mssql_1
mappings:
Utility:
type: "annotation"
# The directory for entity (relative to bundle path)
dir: '%kernel.project_dir%/src/Entity/Utility'
prefix: 'App\Entity\Utility'
alias: Utility
mssql_crm:
connection: mssql_1
mappings:
Crm:
type: "annotation"
# The directory for entity (relative to bundle path)
dir: '%kernel.project_dir%/src/Entity/Crm'
prefix: 'App\Entity\Crm'
alias: Crm
So they are sharing the same connection (but a different em). The user of the connections has the privileges to read/write both databases (but only to alter schema to the fgel_utility DB. Both DB are stored in a SQL Server 2008.
When i'm tryin' to execute
php bin/console doctrine:schema:update --dump-sql
I get this error
The class 'App\Entity\Crm\User' was not found in the chain configured
namespaces App\Entity\Utility, FOS\UserBundle\Model
You can actually trick Doctrine to do cross-database join queries to MySQL/MariaDB, simply by prefixing the database name in the ORM\Table annotation of your entites :
// src/Entity/User.php
#ORM\Table(name="dbname.users")
This will be used by Doctrine in all the SELECT, JOIN statements.
That beeing said, using this solution, the DB_NAME from DATABASE_URL or any other values of your env files won't be used, which can lead to confusions (as the database name should be coupled to the connection, not the entity).
As you cannot resolve dynamic value in your ORM mappings, such as "#ORM\Table(name=%env(DBNAME)%.users"), but here is an exemple of how you can use the LoadClassMetadata event from Doctrine to do that job dynamically.
The class constructor takes the Entities namespace as a first argument, and the database name as the second argument.
When Doctrine runs the metadata loading, it will fire the callback method with the metadata class for each entity, onto which you can process and set the table name dynamically from theses values.
// src/DatabasePrefixer.php
class DatabasePrefixer implements EventSubscriber
{
private $namespace;
private $tablePrefix;
/**
* #param $namespace string The fully qualified entity namespace to add the prefix
* #param $tablePrefix string The prefix
*/
public function __construct($namespace, $tablePrefix)
{
$this->namespace = $namespace;
$this->tablePrefix = $tablePrefix;
}
public function getSubscribedEvents()
{
return ['loadClassMetadata'];
}
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if ($this->namespace == $classMetadata->namespace) {
$classMetadata->setTableName(sprintf('%s.%s', $this->tablePrefix, $classMetadata->table['name']));
}
}
}
Supposing that you have a DB_NAME env variable, configure the class as a service in your config/services.yml, using the yaml resolving features of Symfony, and the event tagging to listen to the correct Doctrine event :
// config/services.yaml
services:
[...]
dbname.prefixer:
class: App\DatabasePrefixer
arguments:
$namespace: 'App\Entity'
$tablePrefix: '%env(DB_NAME)%'
tags:
- { name: doctrine.event_listener, event: loadClassMetadata, lazy: true }
According to this https://github.com/doctrine/doctrine2/issues/6350 cross database joins between different entity managers (same connections) isn't supported.

upload not running with VichUploaderBundle

I would like to use a VichUploaderBundle for upload files in my symfony 3.2.3 project (PHP 5.6). I try lot of thing but nothing upload run. But the persistance layer run perfeclty with my database.
config.yml
knp_gaufrette:
stream_wrapper: ~
adapters:
fileupload_adapter:
local:
directory: %kernel.root_dir%/../web/
create: true
filesystems:
fileupload_fs:
adapter: fileupload_adapter
# VichUploaderBundle Configuration
vich_uploader:
db_driver: orm
twig: true
storage: gaufrette
mappings:
tgmedia_file:
uri_prefix: web
upload_destination: fileupload_fs
namer: vich_uploader.namer_origname
Entity
namespace MediaBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use TweedeGolf\MediaBundle\Model\AbstractFile;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* #ORM\Entity
* #Vich\Uploadable
* #ORM\HasLifecycleCallbacks
* #ORM\Table
*/
class FileUpload
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* NOTE: This is not a mapped field of entity metadata, just a simple property.
*
* #Vich\UploadableField(mapping="tgmedia_file", fileNameProperty="imageName")
*
* #var File
*/
protected $imageFile;
/**
* #ORM\Column(type="string", length=255)
*
* #var string
*/
private $imageName;
/**
* #ORM\Column(type="datetime")
*
* #var \DateTime
*/
protected $updatedAt;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #return File|null
*/
public function getImageFile()
{
return $this->imageFile;
}
/**
* 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 $image
*
* #return Product
*/
public function setImageFile(File $image = null)
{
$this->imageFile = $image;
if ($image) {
// 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 \DateTimeImmutable();
}
return $this;
}
/**
* #param string $imageName
*
* #return FileUpload
*/
public function setImageName($imageName)
{
$this->imageName = $imageName;
return $this;
}
/**
* #return string|null
*/
public function getImageName()
{
return $this->imageName;
}
/**
* Set updatedAt
*
* #param \DateTime $updatedAt
*
* #return FileUpload
*/
public function setUpdatedAt($updatedAt)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get updatedAt
*
* #return \DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
}
The formType
<?php
namespace MediaBundle\Form;
use MediaBundle\Entity\File;
use MediaBundle\Entity\FileUpload;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Vich\UploaderBundle\Form\Type\VichFileType;
use Vich\UploaderBundle\Form\Type\VichImageType;
class FileUploadType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('imageName')
->add('imageFile', VichImageType::class, [
'required' => false,
'allow_delete' => true,
'download_link' => true,
'mapped' => false,
'data_class' => null
])
->add('submit', SubmitType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => FileUpload::class,
'csrf_protection' => false,
));
}
}
Controller
<?php
namespace MediaBundle\Controller;
use MediaBundle\Entity\File;
use MediaBundle\Entity\FileUpload;
use MediaBundle\Entity\Product;
use MediaBundle\Form\FileType;
use MediaBundle\Form\FileUploadType;
use MediaBundle\Form\ProductType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class MainController extends Controller
{
public function indexAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$fileUploaded = new FileUpload();
$form = $this->createForm(FileUploadType::class, $fileUploaded, array(
'action' => $this->generateUrl('media_main')
));
$form->handleRequest($request);
if ($form->isSubmitted())
{
$fileUploaded->setUpdatedAt(new \DateTime());
$fileUploaded->setImageFile($form->get('imageFile')->getData());
$em->persist($fileUploaded);
$em->flush();
}
return $this->render('MediaBundle:main:index.html.twig', array(
'form' => $form->createView()
));
}
}
Where is my error ? What wrong ?
uri_prefix: /project/web/fileupload_fs
upload_destination: '%kernel.root_dir%/../web/fileupload_fs'
uri_prefix begins from www folder.
and change controller like that
if ($form->isSubmitted() && $form->isValid())
{
$em->persist($fileUploaded);
$em->flush();
}

Laravel user_id forign key not working

I am new to Laravel and I have multiple registration types I have tried to add a foreign key following the aravel 5.2 documentation and it keeps giving me errors any help would be greatly appreciated. I need to connect the registration of each type of user into the different registration types. Below I will post one of the three registration types that I have. If there is a way to only add the user information to the user table after there email has been verified that would be great. So in other words each registration (3x) I need them to fill out registration and only the user table info goes to the user table the rest would go to the other table but I want to two to be connected.
This is the migration file I am trying to get to work following the laravel 5.2 documents.
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->boolean('active')->default(false);
$table->rememberToken();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('artists');
$table->foreign('user_id')
->references('id')->on('artists')
//->onUpdate('cascade')
->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::drop('users');
//$table->dropForeign('artists_user_id_foreign');
}
}
I also tried doing the same thing on the artists table and it did not work either keep getting a foreign constraint issue.
Ill need to do that process for each type of user account.
The controller is set up like this for artist
<?php
namespace App\Http\Controllers\Artist;
use App\User;
use App\Artist;
use App\Mailers\AppArtMailer;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class RegistrationController extends Controller
{
//protected $redirectTo = 'art';
/** Create a new registration instance.
*/
public function __construct()
{
//$this->middleware('artist');
}
/** show the Register page/
*
* #return \Response
*/
public function register()
{
return view('art.register');
}
/**
* Perform the registration.
*
* #param Request $request
* #param AppMailer $mailer
* #return \Redirect
*/
public function postRegister(Request $request, AppArtMailer $mailer)
{
//Validate
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:artists',
'password' => 'required'
]);
//create artist
$artist = Artist::create($request->all());
$user = User::create($request->all());
//email them
$mailer->sendEmailConfirmationTo($artist);
//$mailer->sendEmailConfirmationTo($user);
//flash
flash('Please confirm your email address.');
// redirect
return redirect()->back();
}
/**
* Confirm a user's email address.
*
* #param string $token
* #return mixed
*/
public function confirmEmail($token)
{
Artist::whereToken($token)->firstOrFail()->confirmEmail();
flash('You are now confirmed. Please login');
return redirect('artist');
}
}
I also followed the laracast ACL Roles and Permissions but am not sure how to integrate that into each of the user registrations so that each type automatically has the set roles.
Here are the different models I have each is a bit different since I am learning and playing.

Artist Model This will actually change since they can only be an artist if they are a viewer.
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use App\Http\Middleware\RedirectIfAuthenticatedArtist;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class Artist extends Model implements AuthenticatableContract, CanResetPasswordContract
{
protected $table = 'artists';
use Authenticatable, CanResetPassword;
/**
* The database table used by the model.
*
* #var string
*/
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* Boot the model.
*
* #return void
*/
public static function boot()
{
parent::boot();
static::creating(function($artist) {
$artist->token = str_random(30);
});
}
/**
* Set the password attribute.
*
* #param string $password
*/
public function setPasswordAttribute($password)
{
$this->attributes['password'] = bcrypt($password);
}
/**
* Confirm the user.
*
* #return void
*/
public function confirmEmail()
{
$this->verified = true;
$this->token = null;
$this->save();
}
}
This is my users model
<?php
namespace App;
use DB;
use Illuminate\Http\Response;
use App\Http\Controllers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\File;
use Cmgmyr\Messenger\Traits\Messagable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasRoles, Messagable;
//* The attributes that are mass assignable.
//*
// * #var array
// */
protected $fillable = [
'name', 'email', 'password',
];
public function setPasswordAttribute($password)
{
$this->attributes['password'] = bcrypt($password);
}
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token',
];
// $user->roles
}
Sponsors Model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
//use App\Http\Middleware\RedirectIfAuthenticatedSponsor;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class Sponsor extends Model implements AuthenticatableContract, CanResetPasswordContract
{
protected $table = 'sponsors';
use Authenticatable, CanResetPassword;
/**
* The database table used by the model.
*
* #var string
*/
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* Boot the model.
*
* #return void
*/
public static function boot()
{
parent::boot();
static::creating(function($sponsor) {
$sponsor->token = str_random(30);
});
}
/**
* Set the password attribute.
*
* #param string $password
*/
public function setPasswordAttribute($password)
{
$this->attributes['password'] = bcrypt($password);
}
/**
* Confirm the user.
*
* #return void
*/
public function confirmEmail()
{
$this->verified = true;
$this->token = null;
$this->save();
}
}
Viewer Model
<?php
namespace App;
use Illuminate\Auth\Authenticatable;
use Cmgmyr\Messenger\Traits\Messagable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use App\Http\Middleware\RedirectIfAuthenticatedViewer;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class Viewer extends Model implements AuthenticatableContract, CanResetPasswordContract
{
protected $redirectTo = 'viewer';
use Authenticatable, CanResetPassword, Messagable;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'viewers';
//protected $table = 'experience';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token',
];
public function experience()
{
return $this->hasOne('App\Experience');
}
public function awardExperience($points)
{
return $this->experience->award($points);
}
/**
* Boot the model.
*
* #return void
*/
public static function boot()
{
parent::boot();
static::creating(func

Eager fetching with Sonata list view

I'm using SonataAdmin and SonataDoctrineORMAdmin bundles to manage entities.
The problem is I can't figure out how to eager fetch the related entities in the list view and as the number of listed entities increase the number of queries executed increasing rapidly as well.
I tried adding `fetch="EAGER" to the relation annotations but the profiles show that Sonata executes the separate queries anyway.
Here's one relation worth of code:
Post
<?php
namespace Acme\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Table()
* #ORM\Entity
*/
class Post
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255)
**/
private $name;
/**
* #ORM\ManyToMany(targetEntity="Acme\AppBundle\Entity\Tag", fetch="EAGER")
* #ORM\JoinTable(name="join_post_to_tag",
* joinColumns={#ORM\JoinColumn(name="post_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="tag_id", referencedColumnName="id")}
* )
**/
private $tags;
public function getId()
{
return $this->id;
}
public function setName($names)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function setTags($tags)
{
$this->tags = $tags;
return $this;
}
public function __toString()
{
return $this->getName();
}
}
Tag
<?php
namespace Acme\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Table()
* #ORM\Entity
*/
class Tag
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="value", type="string", length=255)
*/
private $value;
public function getId()
{
return $this->id;
}
public function setValue($value)
{
$this->value = $value;
return $this;
}
public function getValue()
{
return $this->value;
}
public function __toString()
{
return ($this->getValue()) ? : '';
}
}
The first related query that is run is fetching all the posts:
SELECT DISTINCT p0_.id AS id0, p0_.id AS id1
FROM Post p0_
LEFT JOIN join_post_to_tag j1_ ON p0_.id = j1_.post_id
LEFT JOIN Tag p1_ ON p1_.id = j1_.target_id
ORDER BY p0_.id ASC
But this does not fetch the related tags or even if it does, it still queries it again:
SELECT t0.id AS id1, t0.value AS value2
FROM Tag t0
INNER JOIN join_post_to_tag ON t0.id = join_post_to_tag.tag_id
WHERE join_post_to_tag.post_id = ?
I tried to mess with the createQuery method in the admin class but could not really find a way to make the related entities fetched correctly.
Is there a way to force the list view to eager fetch the required related entities?
You are on the right track, using the createQuery($context) method.
I have achieved eager loading as following:
public function createQuery($context = 'list')
{
$query = parent::createQuery($context); // let sonata build it's default query for the entity
$rootEntityAlias = $query->getRootAlias(); // get the alias defined by sonata for the root entity
$query->join($rootEntityAlias.'.relationFieldName', 'relationFieldAlias'); // manualy define the join you need
$query->addSelect('relationFieldAlias'); // this is the key line. It is not enough to join a table. You have to also add it to the select list of the query, so that it's actualy fetched
// $query->join(...) // repeat the process of joining and selecting for each relation field you need
// $query->addSelect(...)
return $query; // return the altered query to sonata. this will only work for the "list" action.
}
If you're having trouble using this, let me know:)
Further reads on this topic:
SO question
docs