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.
Related
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?
I updated this entity adding some fields.
Now I'd like to regenerate getters and setters but whe I execute php bin/console make:entity --regenerate App I have no results; my entites are listet but it says "no change". I'v tried event with --overwrite option.
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\ProfessionalRepository")
*/
class Professional
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $nom;
/**
* #ORM\Column(type="boolean")
*/
private $actiu;
/**
* #ORM\ManyToOne(targetEntity="Especialitat")
* #ORM\JoinColumn(referencedColumnName="id")
*/
private $especialitat;
public function getId(): ?int
{
return $this->id;
}
public function getNom(): ?string
{
return $this->nom;
}
public function setNom(string $nom): self
{
$this->nom = $nom;
return $this;
}
public function getActiu(): ?bool
{
return $this->actiu;
}
public function setActiu(bool $actiu): self
{
$this->actiu = $actiu;
return $this;
}
}
I'm using symfony 4.3.8
In previous versions I did it executring 'php bin/console doctrine:genetrate:entities App', I'm not sure if I can use this command in symfony 4, anyway it neither doesn't work.
I'm don't know what else to try...
I have solved it clearing the cache after each change.
php bin/console cache:clear
I installed FOSUserbundle and HWI Oauth bundle.
My problem is: I want to access data from my user entity that is stored in a relation. I'd like to access the data from the fields social_network_slug and social_identifier from UserInSocialNetworks within the FOSUserProvider.
The idea was, that one user can have more that one social network logins. (1:n)- When I log in with my google/facebook etc login, I want to check the table user_in_social_networks if the Id with the social network already exists.
/*
* This is the User class, depending on fos_userBundle
*/
namespace AppBundle\Entity\Registration;
use Doctrine\Common\Collections\ArrayCollection as ArrayCollection;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* One User can have many social networks
* #ORM\OneToMany(targetEntity="UserInSocialNetworks", mappedBy="user", cascade={"remove"})
*/
private $socialnetworks; ....
the Entity Class to store all User's social media logins:
<?php
namespace AppBundle\Entity\Registration;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* UserInSocialNetworks
*
* #ORM\Table(name="user_in_social_networks")
* #ORM\Entity(repositoryClass="AppBundle\Repository\Registration\UserInSocialNetworksRepository")
*/
class UserInSocialNetworks
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* Many Socialnetwork Logins have one User
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Registration\User", inversedBy="socialnetworks")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*
*/
private $user;
/**
* #var int
*
* #ORM\Column(name="social_network_slug", type="string", length=255, nullable=true)
*/
private $socialNetworkSlug;
/**
* #var string
*
* #ORM\Column(name="social_identifier", type="string", length=255, nullable=true)
*/
private $socialIdentifier;
The extended FOSUBUserProvider class:
<?php
namespace AppBundle\Entity\Registration;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use HWI\Bundle\OAuthBundle\Security\Core\User\FOSUBUserProvider as BaseClass;
use Symfony\Component\Security\Core\User\UserInterface;
class FOSUBUserProvider extends BaseClass
{
/**
* {#inheritDoc}
*/
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
// get user_id and socialnetworkname from response
$userIdInSocialNetwork = $response->getUsername();
$socialnetwork = $response->getResourceOwner()->getName();
// Here I'd like to search for an existing $userIdInSocialNetwork
What I checked since now: I can't access the entitymanager in FOSUBUserProvider class, and I can't search that way:
$user = $this->userManager->findUserBy(array(
'socialIdentifier' => $userIdInSocialNetwork,
'social_network_slug' => $socialnetwork)
because it's a relation.
Thanks for any idea!
As you mentioned that you have extended FOSUBUserProvider i assume you have defined a new service for this, If so then you can pass doctrine's entity manager to your class #doctrine.orm.entity_manager. Following HWIOAuthBundle documentation for FOSUserBundle you can pass entity manager as
services:
my.custom.user_provider:
class: MyBundle\Security\Core\User\MyFOSUBUserProvider
arguments: ['#fos_user.user_manager', { facebook: facebook_id }, #doctrine.orm.entity_manager]
And then in your class you can use this service as
use Doctrine\ORM\EntityManager;
use FOS\UserBundle\Model\UserManagerInterface;
//.... other use statements
class FOSUBUserProvider extends BaseClass
{
private $em;
public function __construct(UserManagerInterface $userManager, array $properties, EntityManager $em)
{
$this->em = $em;
parent::__construct($userManager, $properties); /* pass dependencies to parent */
}
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
$this->em->getRepository('AppBundle\Entity\Registration\UserInSocialNetworks')->findBy(....);
/* Do your stuff here */
}
}
Get error on my implementation but resolve this by adding quote to the third argument like bellow:
services:
my.custom.user_provider:
class: MyBundle\Security\Core\User\MyFOSUBUserProvider
arguments: ['#fos_user.user_manager', { facebook: facebook_id }, '#doctrine.orm.entity_manager']
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
))
...
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