Can I search an entity's child properties in Symfony/ORM? - symfony

I'm creating a search function for my Company entity and putting my logic in a custom entity repository using Symfony 2 and Doctrine ORM.
The Company entity has a child collection Locations, as comapnies can have multiple locations, and a Company Name field.
I'm wondering if it's possible to search by Company Name and Location[1].address, Location[2].address, etc. using the Query Builder.
This is what I have so far and can't find a good resource for learning this.
*/ AppBundle/Repository/CompanyRepository.php */
class CompanyRepository extends EntityRepository
{
public function findBySearch($options)
{
$query = $this->createQueryBuilder('c');
foreach ($options as $key => $value) {
$query->where('c.' . $key . ' LIKE :' . $key);
$query->setParameter($key, '%' . $value . '%');
}
return $query->getQuery()->getResult();
}
}
It looks like this doesn't work:
$query->where('c.locations.city LIKE $city);
An example for the $options argument in findBySearch() would be:
[
'companyName' => 'Mikes Company',
'locations.city' => 'New York',
'locations.state' => 'New York'
]
Any advice or links to resources are much appreciated.

You selected c, after that you want to find company with some location, it mean that in company entity you should have (with type Location)field location. Location entity shoud have field (string)city. Field location in Company entity should have oneToMany association with LocationEntity. After that when you will add company's name you also will add company's location. And now you can write:
$query->leftJoin('c.locations', 'locations');
$query->where('locations.city LIKE :city');
$query->setParameter('city', '%'.$city.'%')
If you want to use c.locations.city property of location object you should call there association. Like leftJoin, innerJoin. But I think innerJoin here will be more better and take just two line.
$query->innerJoin('c.locations', 'locations', 'WITH', 'locations.city LIKE (:city)')
$query->setParameter('city', '%'.$city.'%')

try changing $query->where to $query->andWhere
->andWhere() can be used directly, without any ->where() before

Related

EntityType and many to many with extra field relation presented as dropdown (select)

I created a form like that
$builder->add('employees', EntityType::class, [
'class' => ActivityEmployee::class,
'choice_label' => function (ActivityEmployee $employee) {
return sprintf('%s %s', $employee->getEmployee()->getName(), $employee->getEmployee()->getLastName());
},
'multiple' => true,
])
As a result it presents already existing data fine. It shows me all employees with relation to edited activity.
However as choices there should be all employess to choose (employee entity) and as selected data only employess in activityEmployee relation like right now.
I tried to add a query_builder option to provide lists of all employess, but I can only use EntityRepository which means ActivityEmployeesRepository not EmployeesRepository per se.
A can't figure out how to implement it. Basically such relation can be done by CollectionType of custom activityEmployeeType but I'd like to use multi-select for selecting employees.
I can use another approach to not mapping my employees field to entity like that
$currentEmployees = [];
foreach ($activity->getEmployees() as $activityEmployee) {
$currentEmployees[] = $activityEmployee->getEmployee();
}
$builder->add('employees', EntityType::class, [
'class' => Employee::class,
'choice_label' => function (Employee $employee) {
return sprintf('%s %s', $employee->getName(), $employee->getLastName());
},
'mapped' => false,
'multiple' => true,
'data' => $currentEmployees,
]);
It works fine, but I need to deal with updating relation by myself. Which is ok, however I wonder how to achieve such thing in first approach.
Implementation details matter. As far as I can understand you have the following entities:
Activity (entity)
- employees (OneToMany -> ActivityEmployee)
ActivityEmployee (entity)
- activity (ManyToOne -> Activity)
- employee (ManyToOne -> Employee)
Employee (entity)
- activities (OneToMany -> ActivityEmployee) - this one might be missing, actually.
Now you apparently don't hide any implementation details. Meaning, your Activity::getEmployees() returns []ActivityEmployee.
I would have done it like this:
class Activity {
/** #ORM\OneToMany(targetEntity=ActivityEmployee::class) */
private $activityEmployees;
/** #return Employee[] */
public function getEmployees() :Collection {
return $this->activityEmployees->map(function(ActivityEmployee $ae) {
return $ae->getEmployee();
});
}
public function addEmployee(Employee $employee) {
// check, if the employee is already registered, add only then!
if(!$this->getEmployees()->contains($employee)) {
$this->activityEmployees->add(new ActivityEmployee($this, $employee));
}
}
public function removeEmployee(Employee $employee) {
foreach($this->activityEmployees as $activityEmployee) {
if($activityEmployee->getEmployee() === $employee) {
$this->activityEmployees->removeElement($activityEmployee);
}
}
}
}
This way, you hide away how Activity handles the employees and to the outside world (and specifically the PropertyAccessor, that the form component uses) it appears as if Activity has a property employees which are actually Employee[].
If you implement it like this, your first form should actually just work (obviously exchanging ActivityEmployee for Employee) - under the assumption that I didn't make some major mistake. Of course I would also add methods like getActivityEmployees when I would actually specificially need the relation objects.
This whole thing certainly is less beautiful if your many-to-many can contain duplicates.
IF your ActivityEmployee actually has NO other properties besides activity and employee, you could obviously replace the whole thing with a #ORM\ManyToMany and just work with Employee[] instead of the ActivityEmployee[]. However, I assume you have some additional columns like created or something.

Exporting one to many relationship on sonata admin

I have tried to search online for a definite solution to this question but there's really no concrete solution for newbies out there.
I have an Entry which has many EntryListing. In my EntryAdmin listMapper, i comfortably can list entries by a statement as simple as
->add('listings')
which simply returns the listings as defined in the EntryListing __toString() function.
Is there a way to achieve the same when exporting the data by overiding the getExportFields() functions as below:
public function getExportFields()
{
return array('name','tel','email','deviceType','postedOn','createdAt','facilitator','listings');
}
Your help will be very much appreciated
There is another work around you can add a property in your entity which will get all entry listing related to that and in getter function return __toString() of related ones, I had the same scenario for orders and also need the list if products associated with order so i have done it this way by creating exportProducts in orders entity
protected $exportProducts;
public function getExportProducts()
{
$exportProducts = array();
$i = 1;
foreach ($this->getItems() as $key => $val) {
$exportProducts[] = $i .
') Name:' . $val->getProduct()->__toString()() .
' Size:' . $val->getProductsize() .
.../** Other properties */;
$i++;
}
return $this->exportProducts = join(' , ', $exportProducts);
}
And in order admin class i defined exportProducts property in getExportFields() as
public function getExportFields(){
return array(
'Products'=>'exportProducts',
....// Other properties
);
}
In downloaded csv each order contains the list of products under Products cell as comma separated list

How to add custom condition to Sonata global search feature

I would like to add a custom condition to the queries which are generated by Sonata Search feature. The problem is that i have 'status' column which should be set as "active". On the List View i do not have any problem because I am able to set:
protected $datagridValues = array (
'status' => array ('type' => 1, 'value' => Status::ACTIVE)
);
and then all queries check if the status field is set properly.
But the problem is with global search. I can override SearchHandler and force desired behavior, but i can't change any files from vendor/ directory, so i have two questions.
How can i inject my own SearchHandler, which configuration file i need to change and how
Maybe there is a simpler way to develope needed solution?
SOLUTION:
I have figure out how can i inject my own SearchHandler. The following code is used for that:
1. Just edit your services.yml file and put something like that:
cmsbundle.search.handler:
class: XXX\CmsBundle\Search\SearchHandler
arguments:
- #sonata.admin.pool
sonata.admin.block.search_result:
class: XXX\CmsBundle\Search\AdminSearchBlockService
tags:
- { name: sonata.block }
arguments:
- sonata.admin.block.search_result
- #templating
- #sonata.admin.pool
- #cmsbundle.search.handler
Create the file "XXX\CmsBundle\Search\AdminSearchBlockService" and change SearchHandler instance to yours own
Create the file "XXX\CmsBundle\Search\SearchHandler" and change implementation. It can be something like that:
foreach ($datagrid->getFilters() as $name => $filter) {
/** #var $filter FilterInterface */
if ($filter->getOption('global_search', false)) {
if ($filter->getName() !== 'status') {
$filter->setCondition(FilterInterface::CONDITION_OR);
$datagrid->setValue($name, null, $term);
} else {
$filter->setCondition(FilterInterface::CONDITION_AND);
$datagrid->setValue($name, null, 'active');
}
$found = true;
}
}
IMPORTANT
'status' field must be added to configureDatagridFilters method in Admin class.
I thought I'd add my solution to this problem.
My problem was similar, my Admin class would modify the Admin Entities' respective createQuery. This query would add in restrictions so that the user can only view their models, or only view things which are not deleted for example.
The problem is the SearchHandler.php would set ALL filters as
$filter->setCondition(FilterInterface::CONDITION_OR);
This would cause queries to look like:
( myAddedCondition OR filterCondition OR filterCondition OR filterCondition )
What I really wanted instead was:
( myAddedCondition ) AND ( filterCondition OR filterCondition OR filterCondition )
In order to achieve this, I registered a GLOBAL ASTWalker which iomplements the walkWhereClause method.. Then in the Walker I would manually edit the generated SQL to suit my requirements.

Why to use doctrine relationships vs native query

I have a simple question. I saw a Symfony2-Tutorial where the blogger used everywhere simple entities without relations and was also using doctrine native-query to do joins and query the different entities.
What is better, to use doctrine native query or build every time relationships ?
Greetings Michael
I think that it depends on how the resultset will be handle. For example, suppose the following relationship:
a company has a lot of employees (1:N)
Just figure out the impact if this company has more than 10K employees when it does a select query in every request loading all employees data to be showed. In this scenario could be a good practice to create a lazy association in models:
<?php
/**
* #Entity
*/
class Company
{
/**
* #OneToMany(targetEntity="Employee", fetch="EXTRA_LAZY")
*/
public $employees;
}
At this case, doctrine only will triggered the required data from database, because it knows that all the data will be accessed gradually as you request it. You can read more about lazy associations in doctrine's docs.
How about the native queries?
Native queries can map arbitrary SQL code to objects, such as highly vendor-optimized SQL or stored-procedures. Fast scalar results and less ram usage. Take note that a complex model relationship could be too heavy for server to be manipulated. For example, look this structure based on Class Table Inheritance:
There is a super class called Product and there are more than 200 different sub-products that extends from Product. Each sub-product is storaged in its respective table.
<?php
abstract class Product
protected $name;
Some sub-products as example:
<?php
class Candy extends Product
/** specific property for this product */
private $sugarLevel;
Another one:
<?php
class IceCream extends Product
/** specific property for this product */
private $temperature;
Now, you need to assess every product in you depot. Normally, the first idea to get this resulst is doing:
$assess = array();
$products = $em->getRepository('models\Product');
foreach ($products as $p)
{
//summarize each product by type
$assets[$p->getType()] = $assets[$p->getType()] + 1;
}
echo "There are " . $assets['candy'] " candies in stock";
This is really heavy process because we are quering 200 tables just to determinate the existence of each product. This could be easy mitigated with a simple native query:
$query = $em->createNativeQuery('SELECT p.type, count(p.type) as total FROM Product p group by p.type', $rsm);
$result = $query->getArrayResult();
print_r($result);
// [0] => array('type' => 'candy', 'total' => 545),
// [1] => array('type' => 'icecream', 'total' => 344),
//...
// [199] => array('type' => 'foo', 'total' => 878),

Get the name of all the tables on my database

I need get the name of all the tables that exist on my database. I am usin Propel like my ORM. Actully i have been trying on this form.
$dbmap = \Propel::getDatabaseMap('data');
$tablastmp = $dbmap->getTables();
$tablas = array();
foreach ($tablastmp as $tablatmp) {
$tablas[] = $tablatmp->getName();
}
echo '<pre>';
print_r($tablas);
echo '</pre>';
die();
but this return an array that is empty.
array();
And I need that return something like that:
array( [0] => 'clients', [1] => 'workers' );
Please someone help. I have been trying that for a few days.
Actually, you probably wouldn't use Propel to get this information as Propel only loads the table map information when the table is actually used.
The original Schema file is used during the build phase (running 'propel_gen om' etc). The runtime part of Propel never looks at the Schema file, so there is no way to query it per se.
The answer to your question is to look at the database, e.g. the MySQL query to list the tables in a database:
SHOW [FULL] TABLES [{FROM | IN} db_name]
[LIKE 'pattern' | WHERE expr]
This might help you (hopefully)
$em = $this->getDoctrine()->getManager();
foreach ($em->getMetadataFactory()->getAllMetadata() as $md) {
var_dump($md->getName()); // dump the full class names
var_dump($md->getTableName()); // dump the table names
}

Resources