In Symfony I'm using default ORM Doctrine, but this tool can't give me enough methods to manipulate with different cases. I want to write my own classes and using something like DBAL, just for connections making custom SQL queries and fetch the result. Who can give me some examples? Which classes I should use to make my model layer, extend my functionality.
If you want to write your own SQL query with doctrine you can check that doc page:
http://doctrine-orm.readthedocs.org/en/latest/reference/native-sql.html
but most of the time DQL is more than enough
this is from a project I did, ignore my sql query
//Daily Alerts
$dailyAlertsQuery = $em
->createQuery("
SELECT COUNT (a) AS daily
FROM XXX\xxxBundle\Entity\Alert a
JOIN a.user u
WHERE a.mentionUpdate = '1'
AND u.isActive = '1'
")
->getResult();
$dailyAlerts = new ArrayCollection($dailyAlertsQuery);
As a good practise, you may put all your custom queries (SQL or DQL) in an EntityRepository
Here is an example of a custom EntityRepository
use Doctrine\ORM\EntityRepository;
use \Doctrine\Common\Collections\Criteria;
/**
* RequestRepository
*
*/
class RequestRepository extends EntityRepository
{
public function findByStatus($status = array(), $limit = 5, $orderBy = null)
{
$queryBuilder = $this->_em->createQueryBuilder();
$queryBuilder
->select('n')
->from('BaseBundle:Request', 'n')
->where('n.status IN (:status)')
->setParameter('status', $status)
->setMaxResults($limit);
if (!is_null($orderBy)) {
$queryBuilder->orderBy('n.' . $orderBy[0], $orderBy[1]);
}
$lines = array();
foreach ($queryBuilder->getQuery()->getResult() as $line) {
$lines[] = $line;
}
return $lines;
}
public function getByExpressions($expressions = array(), $limit = 5, $orderBy = null)
{
$criteria = Criteria::create();
if (!empty($expressions)) {
foreach ($expressions as $expression) {
$criteria->andWhere($expression);
}
}
$criteria->setMaxResults($limit);
if (!is_null($orderBy)) {
$criteria->orderBy($orderBy);
}
return $this->matching($criteria);
}
}
And in the Entity code, you define this custom repository as follows:
use Doctrine\ORM\Mapping as ORM;
/**
* Request
*
* #ORM\Table(name="request")
* #ORM\Entity(repositoryClass="BaseBundle\Repository\RequestRepository")
*/
class Request
{
//Entity code, irrelevant here
}
Related
I have an SQL query that looks like:
SELECT * FROM orders FORCE INDEX(order_type_index) WHERE `type` = 1
I can use query builder to recreate this:
DB::table(DB::raw('orders force index(orders_type_index)'))
->where('type', 1)
->get();
But is there anyway to make Eloquent use this index, like:
Order::forceIndex('orders_type_index')->where('type', 1)->get();
You can do this by creating a local scope that changes the builder's table name when applied. The table should be checked for the index before it's used. You could throw an exception or ignore it if an invalid index name is supplied (I chose the latter approach.)
<?php
namespace App;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/*
* Class Order
*
* #method \Illuminate\Database\Eloquent\Builder useIndex(string $index)
* #method \Illuminate\Database\Eloquent\Builder forceIndex(string $index)
*/
class Order extends Model
{
private function tableIndexExists(string $index): boolean
{
$table = $this->getTable();
$index = strtolower($index);
$indices = Schema::getConnection()
->getDoctrineSchemaManager()
->listTableIndexes($table);
return array_key_exists($index, $indices);
}
public function scopeUseIndex(Builder $query, string $index): Builder
{
$table = $this->getTable();
return $this->tableIndexExists($index)
? $query->from(DB::raw("`$table` USE INDEX(`$index`)"))
: $query;
}
public function scopeForceIndex(Builder $query, string $index): Builder
{
$table = $this->getTable();
return $this->tableIndexExists($index)
? $query->from(DB::raw("`$table` FORCE INDEX(`$index`)"))
: $query;
}
}
If you need to do this on multiple models, it can easily be added to a trait and imported. The docblock ensures that your IDE is aware of the magic methods for code completion.
Then you can use it like this:
$orders = Order::forceIndex("orders_type_index")->where("type", 1)->get();
// or this:
$orders = Customer::find(234)
->orders()
->forceIndex("orders_type_index")
->where("type", 1)
->get();
https://packagist.org/packages/shaburov/laravel-mysql-index-hints-scope
Order::useIndex('test_index')
->ignoreIndex('test_index')
->useIndex(['test_index', 'example_index']);
I'd like to define a "global" method that can be used by multiple controllers and commands. Where should it be placed in Laravel 5.4?
Let's say I have the following controller. How would I call the "global" method instead, and where would that "global" method be located exactly?
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Flight;
class FlightsController extends Controller
{
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Index
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$flights = Flight::where('active', 1)
->orderBy('name', 'desc')
->take(10)
->get();
foreach ($flights as $flight) {
if ( $flight->price == 0 )
{
$output = "some value";
}
else
{
$output = "some other value";
}
}
return view('flights.index')
->with(['output' => $output])
;
}
}
When you want a method that fetches many models, and you want to use it in many places, put it in a Repository:
class FlightRepository
{
public function getLastTenFlights()
{
return Flight::where('active', 1)
->orderBy('name', 'desc')
->take(10)
->get();
}
}
For example from your controller:
public function index( FlightRepository $repo )
{
$flights = $repo->getLastTenFlights();
//if you want you can put this additional login in the method too...
foreach ($flights as $flight) {
if ( $flight->price == 0 )
{
$output = "some value";
}
else
{
$output = "some other value";
}
}
return view('flights.index')
->with(['output' => $output])
;
}
You can create a Object and call the object when you want.
See example:
FlighRepository = new FlighRepository;
FlighRepository->index();
I personally prefer query scopes to repositories, so I would do something like this:
class Flight extends Model
{
// model setup
/**
* Scope query to get last 10 flights.
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeLastTen($query)
{
return $query->where('active', 1)->orderBy('name', 'desc')->take(10);
}
// rest of model
}
And you can use it similarly to how you're currently using it, only it's more readable:
$flights = Flight::lastTen()->get();
This also has the advantage of being able to chain other queries off of it. Say, for example, you wanted the last ten American Airlines flights, you could do:
$flights = Flight::lastTen()->where('airline', 'American')->get();
// equivalent to
// $flights = Flight::where('airline', 'American')->lastTen()->get();
I think that service is the best option to store the functionality which is shared between controllers and commands. You can access them using Service Container (https://laravel.com/docs/5.5/container).
actually im working on a project, where i want to have all DB-tables as Models. But now im stucking at one Point.
Lets say i have a "Master"-Table where many different relations are defined like the following (easy example):
Human has one heart; Human has one brain... and so on...
Is it possible, to fill up the Master-Model with other Models?
In PHP it would looks like that:
$human = new Human();
$human->heart = new Heart();
$human->brain = new Brain();
Finally i want to say:
$human-save(TRUE);
to VALIDATE all relational models AND save all relational data and the human object in DB.
Is that possible? I cant find something like that on the whole internet O_o.
Thank you very much!
You can override ActiveModel Save method, according to docs:
public function save($runValidation = true, $attributeNames = null)
{
if ($this->getIsNewRecord()) {
$save = $this->insert($runValidation, $attributeNames);
} else {
$save = $this->update($runValidation, $attributeNames) !== false;
}
/* Condition Work if heart and brain is also ActiveModel then
you can trigger save method on these models as well
or you can add your custom logic as well.
*/
if($this->heart && $this->brain) {
return $this->heart->save() && $this->brain->save();
}
return $save;
}
I suggest you following approach:
Let's say you have same relation names as property names for nested objects (some rule needed to call $model->link() method)
Declare common class for Models with nested Models (for example ActiveRecordWithNestedModels)
Override in common class methods save and validate to perform cascade for these operations (using reflection)
Let your models will inherit this common class
Or, as an alternative for overriding validate method, you can build some suitable implementation for rules method in common class.
This common class can looks as follows (this is a simple draft, not tested, just to show the conception):
<?php
namespace app\models;
use yii\db\ActiveRecord;
class ActiveRecordWithNestedModels extends ActiveRecord
{
public function save($runValidation = true, $attributeNames = null)
{
$saveResult = parent::save($runValidation, $attributeNames);
$class = new \ReflectionClass($this);
foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
$propertyValue = $property->getValue($this);
if (!empty($propertyValue) && is_subclass_of($propertyValue, ActiveRecord::className())) {
/* #var ActiveRecord $nestedModel */
$nestedModel = $propertyValue;
$nestedModel->save($runValidation);
$relation = $property->name;
$this->link($relation, $nestedModel);
}
}
return $saveResult;
}
public function validate($attributeNames = null, $clearErrors = true)
{
$class = new \ReflectionClass($this);
foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
$propertyValue = $property->getValue($this);
if (!empty($propertyValue) && is_subclass_of($propertyValue, ActiveRecord::className())) {
/* #var ActiveRecord $nestedModel */
$nestedModel = $propertyValue;
if (!$nestedModel->validate(null, $clearErrors)) {
array_push($this->errors, [
$property->name => $nestedModel->errors
]);
}
}
}
parent::validate($attributeNames, $clearErrors);
if ($this->hasErrors()) return false;
return true;
}
}
Then your models can looks like this:
class Heart extends ActiveRecordWithNestedModels
{
}
class Human extends ActiveRecordWithNestedModels
{
/* #var Heart $heart */
public $heart = null;
/**
* The relation name will be 'heart', same as property `heart'
*
* #return \yii\db\ActiveQuery
*/
public function getHeart()
{
return $this->hasOne(Heart::className(), ['id', 'heart_id']);
}
}
And (in theory) you can do:
$human = new Human();
$human->heart = new Heart();
$human->save();
P.S. here can be many complex details in further implementation, as for example
using transactions to rollback save if some child object fails save
overriding delete
serving one-to-many and many-to-many relations
skip cascade if property has no corresponding relation
serving $attributeNames in cascade operations
etc
I am working on the symfony 2.5 project with doctrine 2.4.
I want to cache query result with cache id and cache time, so I can delete the cache result, whenever needed though admin.
I am able to cache the query result with "createQueryBuilder()" option.
Example:
$this->createQueryBuilder('some_table')
->select('some_table')
->where('some_table.deleted_date IS NULL')
->getQuery()
->useResultCache(true)
->setResultCacheLifetime(120) //Query Cache lifetime
->setResultCacheId($queryCacheId) //Query Cache Id
->getResult();
But I am not able to find the similar way to chache query result for "findOneBy()" option.
Example:
$this->findOneBy(array('some_field'=>$some_value));
I am looking some proper solution, any help is much appreciated.
You need to redefine every findBy* or findOneBy* function into custom repository: this is the only way as doctrine2 default behaviour doesn't take into account this situation.
Is up to you, unfortunately.
Also Ocramius (a Doctrine2 devel) say it here https://groups.google.com/d/msg/doctrine-user/RIeH8ZkKyEY/HnR7h2p0lCQJ
Make a common function for this.
$repo->findOneByYourSufff($yourStuff, $yourRepoClass);
Followed by your common function :
public function findOneByYourSufff($yourStuff, $yourRepoClass) {
$q = $this->createQueryBuilder()
->select('x.*')
->from($yourRepoClass, 'x');
foreach($yourStuff as $fieldKey => $wh) {
$q->andWhere("b.$fieldKey = :fieldName");
$q->setParameter("fieldName", $wh);
}
$q->useResultCache(true)
->setResultCacheLifetime(120) //Query Cache lifetime
->setResultCacheId($queryCacheId) //Query Cache Id
->getSingleResult();
return $q
}
Here is an example of how you could cache results from a single repository for the functions:
findOneBy(['foo' => 'bar'])
findOneByFoo('bar')
findOneBy(['bar' => 'foo', 'foo' => 'bar')
etc...
It overrides the EntityRepository::FindOneBy function. It follows the same signature so there is no need to update the invoking code. All FindOneBy% type calls will pass through our implementation of findOneBy.
<?php
/**
* MyObject Repo
*/
namespace MyLib\Entity\Repository;
use Doctrine\ORM\EntityRepository;
class MyObjectRepository extends EntityRepository
{
const CACHE_KEY = 'my_object';
const ALIAS = 'my_object';
/**
* Override - use cache.
*
* #param array $criteria
* #param array|null $orderBy
* #return mixed
*/
public function findOneBy(array $criteria, array $orderBy = null)
{
$queryBuilder = $this->createQueryBuilder(self::ALIAS);
foreach($criteria as $field => $value) {
$queryBuilder->andWhere(self::ALIAS . ".{$field} = :{$field}")->setParameter($field, $value);
}
if (is_array($orderBy)) {
foreach ($orderBy as $field => $dir) {
$queryBuilder->addOrderBy($field, $dir);
}
}
$queryBuilder->setMaxResults(1);
$query = $queryBuilder->getQuery();
$query->useResultCache(true, 3600, self::CACHE_KEY);
$result = $query->getResult();
if ($result) return reset($result);
return $result;
}
/**
* Depending how you hydrate the entities may make more
* sense to use cache layer at findAll
*
* #param void
* #return array The entities.
*/
public function findAll()
{
$query = $this->getEntityManager()->createQuery('select v from \OAS\Entity\MyObject v');
$query->useResultCache(true, 3600, self::CACHE_KEY);
$result = $query->getResult();
return $result;
}
/**
*
*/
public function invalidateCache()
{
//this would depend on your cache implementation...
$container = \Zend_Registry::get('doctrine');
$cache = $container->getCacheInstance();
$cache->delete(self::CACHE_KEY);
}
}
This could be done in a more OOP fashion of course, extending an intermediary class, if you wanted to say have a property on the repository which simply turned the cache on or off. You could extend a similar approach for other repository operations functions.
I have a line like this
// $repository is my repository for location data
$locationObject = $repository->findOneBy(array('name' => $locationName));
Which selects the first record it can find from the Locations table. Which is fair enough.
However, I have some additional data in that table to make the query more precise. Specifically, an "item_name" column. In the Location class it is specified as such:
/**
* #ORM\ManyToOne(targetEntity="Item", inversedBy="locations", cascade={"persist", "remove"})
* #ORM\JoinColumn(name="item_id", referencedColumnName="item_id", onDelete="CASCADE")
*/
protected $item;
So there is also an Item table with item_id, item_name, etc.
What I want to do is change the original findOneBy() to also filter by item name. So I want something like:
$locationObject = $repository->findOneBy(array('name' => $locationName, 'item' => $itemName));
But because $item is an object in the Locations class rather than a string or an ID obviously that wouldn't work. So really I want to somehow much against item->getName()...
I'm not sure how I can do this. Does anyone have any suggestions?
Thanks
I guess you must create a custom query with join. It's better you create a custom repository class for this entity and then creates a custom query build inside it.
Entity:
// src/AppBundle/Entity/Foo.php
/**
* #ORM\Table(name="foo")
* #ORM\Entity(repositoryClass="AppBundle\Repository\FooRepository")
*/
class Foo
{
...
}
Your repository:
// src/AppBundle/Repository/FooRepository.php
use Doctrine\ORM\EntityRepository;
class FooRepository extends EntityRepository
{
public function findByYouWant($id)
{
// your query build
}
}
Controller:
// src/AppBundle/Controller/FooController.php
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller
{
public function showAction()
{
// ... your code
$locationObject = $repository->findByYouWant($id);
}
}
You should add a method to your Location repository class, and create a query similiar to the one below:
class LocationRepository extends EntityRepository
{
public function findLocationByItemName($locationName, $itemName)
{
$qb = $this->createQueryBuilder('location');
$qb->select('location')
->innerJoin(
'MyBundle:Item',
'item',
Query\Expr\Join::WITH,
$qb->expr()->eq('location.item', 'item.item_id')
)
->where($qb->expr()->like('location.name', ':locationName'))
->andWhere($qb->expr()->like('item.name', ':itemName'))
->setParameter('locationName', $locationName)
->setParameter('itemName', $itemName);
$query = $qb->getQuery();
return $query->getResult();
}
}
You have to use a custom dql.You can construct it using the querybuilder.
//in your controller
protected function getEntities($itemName){
$em = $this->get('doctrine.orm.default_entity_manager');
$qb = $em->createQueryBuilder();
$qb->select('a')->from('YourBundleAlias:YourEntityName', 'a')->join('a.item','b')->where('b.item = :item')->setParameter('item', $itemName);
return $qb->getQuery()->execute();
}
This is as easy as:
$locationObject = $repository->findOneBy(array(
'name' => $locationName,
'item' => $itemObject
));
Using Doctrine2 in order to do a findBy on a related entity field you must supply an entity instance: $itemObject.