How to dry-up similar Doctrine functions from two Entity Repositories? - php

I'm using Symfony 4.
namespace App\Repository;
use ...
class CountryRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, Country::class);
}
...
public function deleteMultipleByIds(array $ids): void
{
$builder = $this->createQueryBuilder('l');
$builder->delete()
->where('l.id IN(:ids)')
->setParameter(
':ids',
$ids,
Connection::PARAM_INT_ARRAY
);
$query = $builder->getQuery();
$query->execute();
}
Same method exists in CountryI18nRepository class.
I'd want there to be just one function like that, which will just use correct Entity (Country v CountryI18n).
How and where do I create a new class? Should that class be of ServiceEntitiyRepository class or otherwise which?

If your problem is about duplication, you can make a GenericRepo (not necessarily a doctrine repository; please choose a better name) that you can inject and use where you need.
Something like
class GenericRepo
{
public function deleteMultipleByIds(QueryBuilder $qb, string $rootAlias, array $ids): void
{
$qb->delete()
->where(sprintf('%s.id IN(:ids)', $rootAlias))
->setParameter(':ids', $ids, Connection::PARAM_INT_ARRAY);
$qb->getQuery()->execute();
}
}
And in your, for instance CountryI18nRepository
class CountryI18nRepository
{
private $genericRepo;
public function __construct(GenericRepo $genericRepo)
{
$this->genericRepo = $genericRepo;
}
public function deleteMultipleByIds(array $ids): void
{
$builder = $this->createQueryBuilder('l');
$this-> genericRepo->deleteMultipleByIds($builder, 'l', $ids);
}
}
You could also extend from GenericRepo but, since PHP only supports single inheritance, is better (at least in my opinion) to use the composition as shown above.
Disclaimer
I didn't tested this code so it is possible that some adjustment will needed. Concepts shown btw are valid.

create an abstract repository with the deleteMultipleByIds like :
abstract class BaseCountryRepository extends ServiceEntityRepository
and extend it instead of ServiceEntityRepository in the other CountryRepositories
class CountryRepository extends BaseCountryRepository
class CountryI18nRepository extends BaseCountryRepository
you can remove the deleteMultipleByIds definition from these classes

Related

Abstract Class and Dependency Injection in Shopware 6, Symfony

Currently I am working on Shopware 6 extension, which is based on Symfony. What I don’t understand, is how to implement abstract classes and dependency injection.
So I want to be able to refactor the code, and to use those methods often, but in another context (with another repository)
<?php
declare(strict_types=1);
namespace WShop\Service;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Uuid\Uuid;
/**
* Service for writing Products
*/
class ProductService
{
private EntityRepository $productRepository;
private MediaImageService $mediaImageService;
private EntityRepository $productMediaRepository;
public function __construct(
EntityRepository $productRepository,
MediaImageService $mediaImageService,
EntityRepository $productMediaRepository
)
{
$this->productRepository = $productRepository;
$this->mediaImageService = $mediaImageService;
$this->productMediaRepository = $productMediaRepository;
}
private function createProduct(array $data, Context $context = null): void
{
$context = $context ?? Context::createDefaultContext();
$this->productRepository->create([
$data
], $context);
}
public function updateProduct(array $data): void
{
$this->productRepository->update([$data], Context::createDefaultContext());
}
public function getExistingProductId(string $productNumber): ?string
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('productNumber', $productNumber));
return $this->productRepository->searchIds($criteria,
Context::createDefaultContext())->firstId();
}
}
As you can see, there are dependency injection inside construct (Product Repository). Now my question is, how am I able to create abstract class, that is storing those methods, but the child classes is going to kind of "rewrite" parent construct with repository that is needed? For example, I want to use getDataId (Now it is called getExistingProductId, but it is going to be refactored and renamed in abstract class) method on product repository, but for the next class I want to use the same method on categors repository?
Service.xml aka Dependency Injector
<service id="wshop_product_service" class="WShop\Service\ProductService">
<argument type="service" id="product.repository"/>
<argument id="wshop_media_image_service" type="service"/>
<argument type="service" id="product_media.repository"/>
</service>
I am kind of new into OOP. Please provide good example and code explanation. Thanks!
If I understood you correctly, you just want the first argument to be interchangeable and the 3 methods in your example should be implemented in the abstract. Here's one idea for that.
The abstract:
abstract class AbstractEntityService
{
protected EntityRepository $repository;
public function __construct(EntityRepository $repository)
{
$this->repository = $repository;
}
public function create(array $data, ?Context $context = null): void
{
$context = $context ?? Context::createDefaultContext();
$this->repository->create([
$data
], $context);
}
public function update(array $data): void
{
$this->repository->update([$data], Context::createDefaultContext());
}
abstract public function getDataId(array $params): ?string;
protected function searchId(Criteria $criteria): ?string
{
return $this->repository->searchIds(
$criteria,
Context::createDefaultContext()
)->firstId();
}
}
You take the repository in the constructor and implement all your general methods regarding the generic repositories in the abstract. The getDataId method you want to implement in the extending class, since you use a specific criteria for each one (presumably). So you just force the implementation in the extending class by defining an abstract signature.
Your service class:
class ProductService extends AbstractEntityService
{
private MediaImageService $mediaImageService;
private EntityRepository $productMediaRepository;
public function __construct(
EntityRepository $productRepository,
MediaImageService $mediaImageService,
EntityRepository $productMediaRepository
) {
parent::__construct($productRepository);
$this->mediaImageService = $mediaImageService;
$this->productMediaRepository = $productMediaRepository;
}
public function getDataId(array $params): ?string
{
if (!isset($params['productNumber'])) {
return null;
}
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('productNumber', $params['productNumber']));
return $this->searchId($criteria);
}
// your other methods using the injected services
}
In the extending class you pass only the repository to the parent constructor since the other injected services are used only in this specific instance. You implement getDataId where you create your specific criteria and call the protected (since it should only be used by extensions) searchId method with the criteria.

Doctrine SQLFilter with QueryBuilder in Symfony not working

I'm trying to create an SQLFilter for a query in my Symfony app.
The issue is that the filter is not applied on the query (and not called), even though is it enabled correctly (see below).
The repository is not linked to an entity, because the database is external to my app, but it still has access to the data.
Am I missing something ?
Here's the filter:
<?php
namespace App\SQL\Filter;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
class UserRoleFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
return 'c.roleId = 1';
}
}
I registered it in config/packages/doctrine.yaml:
doctrine:
filters:
user_role: App\SQL\Filter\UserRoleFilter
The controller:
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Doctrine\Persistence\ManagerRegistry;
use App\Repository\CustomerRepository;
class CustomerController extends AbstractController
{
public function myAction(Request $request, ManagerRegistry $doctrine, CustomerRepository $customerRepository)
{
$doctrine->getManager()->getFilters()->enable('user_role');
$customers = $customerRepository->findAll();
}
}
The repository:
<?php
namespace App\Repository;
use Doctrine\DBAL\Connection;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
class CustomerRepository
{
protected Connection $conn;
protected ObjectManager $em;
public function __construct(ManagerRegistry $doctrine)
{
$this->em = $doctrine->getManager();
$this->conn = $this->em->getConnection();
}
public function findAll(): array
{
dump($this->em->getFilters()->isEnabled('user_role')); // returns true
return $this->conn->createQueryBuilder()
->select('c.*')
->from('customer', 'c')
->executeQuery()
->fetchAllAssociative();
}
}
From looking at the source for Doctrine/DBAL, it doesn't look like the filter would ever be applied to the query you are executing.
Within your repository class you are creating an instance of Doctrine\DBAL\Query\QueryBuilder which only holds a reference to Doctrine\DBAL\Connection.
Then the select data is set to its private parameter $sqlParts.
When executeQuery() is called is concatenates the content of sqlParts into a string with no mention or reference to any filter objects nor their constraints. This can be seen on line 308 of the QueryBuilder class.
QueryBuilder::executeQuery()
You can also see how the select query is concatenated on line 1320 of QueryBuilder.
QueryBuilder::getSQLForSelect()
The only way I can see to add it easily would be to add it directly to a where clause, e.g.
public function findAll(): array
{
return $this->conn->createQueryBuilder()
->select('c.*')
->from('customer', 'c')
->where("c.roleId = 1") // Or pull it from the filter object in some way
->executeQuery()
->fetchAllAssociative();
}
If you want to see where the filter constraints are added to the queries you can find that data in the ORM package from Doctrine, however these are all linked to entities and table aliases.
SqlWalker::generateFilterConditionSQL()
BasicEntityPersistor::generateFilterConditionSQL()
ManyToManyPersistor::generateFilterConditionSQL()

New alternative for getDoctrine() in Symfony 5.4 and up

As my IDE points out, the AbstractController::getDoctrine() method is now deprecated.
I haven't found any reference for this deprecation neither in the official documentation nor in the Github changelog.
What is the new alternative or workaround for this shortcut?
As mentioned here:
Instead of using those shortcuts, inject the related services in the constructor or the controller methods.
You need to use dependency injection.
For a given controller, simply inject ManagerRegistry on the controller's constructor.
use Doctrine\Persistence\ManagerRegistry;
class SomeController {
public function __construct(private ManagerRegistry $doctrine) {}
public function someAction(Request $request) {
// access Doctrine
$this->doctrine;
}
}
You can use EntityManagerInterface $entityManager:
public function delete(Request $request, Test $test, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$test->getId(), $request->request->get('_token'))) {
$entityManager->remove($test);
$entityManager->flush();
}
return $this->redirectToRoute('test_index', [], Response::HTTP_SEE_OTHER);
}
As per the answer of #yivi and as mentionned in the documentation, you can also follow the example below by injecting Doctrine\Persistence\ManagerRegistry directly in the method you want:
// src/Controller/ProductController.php
namespace App\Controller;
// ...
use App\Entity\Product;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
/**
* #Route("/product", name="create_product")
*/
public function createProduct(ManagerRegistry $doctrine): Response
{
$entityManager = $doctrine->getManager();
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(1999);
$product->setDescription('Ergonomic and stylish!');
// tell Doctrine you want to (eventually) save the Product (no queries yet)
$entityManager->persist($product);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new product with id '.$product->getId());
}
}
Add code in controller, and not change logic the controller
<?php
//...
use Doctrine\Persistence\ManagerRegistry;
//...
class AlsoController extends AbstractController
{
public static function getSubscribedServices(): array
{
return array_merge(parent::getSubscribedServices(), [
'doctrine' => '?'.ManagerRegistry::class,
]);
}
protected function getDoctrine(): ManagerRegistry
{
if (!$this->container->has('doctrine')) {
throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
}
return $this->container->get('doctrine');
}
...
}
read more https://symfony.com/doc/current/service_container/service_subscribers_locators.html#including-services
In my case, relying on constructor- or method-based autowiring is not flexible enough.
I have a trait used by a number of Controllers that define their own autowiring. The trait provides a method that fetches some numbers from the database. I didn't want to tightly couple the trait's functionality with the controller's autowiring setup.
I created yet another trait that I can include anywhere I need to get access to Doctrine. The bonus part? It's still a legit autowiring approach:
<?php
namespace App\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Symfony\Contracts\Service\Attribute\Required;
trait EntityManagerTrait
{
protected readonly ManagerRegistry $managerRegistry;
#[Required]
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
{
// #phpstan-ignore-next-line PHPStan complains that the readonly property is assigned outside of the constructor.
$this->managerRegistry = $managerRegistry;
}
protected function getDoctrine(?string $name = null, ?string $forClass = null): ObjectManager
{
if ($forClass) {
return $this->managerRegistry->getManagerForClass($forClass);
}
return $this->managerRegistry->getManager($name);
}
}
and then
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\Foobar;
class SomeController extends AbstractController
{
use EntityManagerTrait
public function someAction()
{
$result = $this->getDoctrine()->getRepository(Foobar::class)->doSomething();
// ...
}
}
If you have multiple managers like I do, you can use the getDoctrine() arguments to fetch the right one too.

Zend Framework 2 - Showing the content of a database

I'm making some kind of market site with Zend Framework 2. The home got a slider showing all the products (realized with CSS3 keyframes) and some text. Both the sliding pictures and the text are read from a MySQL database. But as result, i get no output but also no errors. The slider gets as many pictures as database rows, but still no content is echoed; plus if i try to change things (like db credentials or getter functions in model) it throws errors as expected, so it clearly reads the db and the problem is elsewhere.
Db for text has 3 fields:
id
name
text
Model for text (Home.php; there's an HomeInterface.php defining all the functions)
<?php
namespace Site\Model;
class Home implements HomeInterface {
protected $id;
protected $name;
protected $text;
public function getId() {
return $this->id;
}
public function getName() {
return $this->name;
}
public function getText() {
return $this->text;
}
}
?>
Mapper for text
<?php
namespace Site\Mapper;
use Site\Model\HomeInterface;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\Adapter\Driver\ResultInterface;
use Zend\Stdlib\Hydrator\HydratorInterface;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\Sql\Sql;
use Zend\Stdlib\Hydrator\ClassMethods;
class TextMapper implements TextMapperInterface {
protected $homePrototype;
protected $adapter;
protected $hydrator;
public function __construct(AdapterInterface $adapter, HomeInterface $homePrototype, HydratorInterface $hydrator) {
$this->adapter = $adapter;
$this->homePrototype = $homePrototype;
$this->hydrator = $hydrator;
}
public function find($name) {
$sql = new Sql($this->adapter);
$select = $sql->select();
$select->from("mono");
$select->where(array("name = ?" => $name));
$stmt = $sql->prepareStatementForSqlObject($select);
$result = $stmt->execute();
if ($result instanceof ResultInterface && $result->isQueryResult() && $result->getAffectedRows()) {
return $this->hydrator->hydrate($result->current(), $this->homePrototype);
}
throw new \InvalidArgumentException("{$name} non esiste.");
}
}
?>
Mapper for text has a factory, since it has dependencies:
<?php
namespace Site\Factory;
use Site\Mapper\TextMapper;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Site\Model\Home;
use Zend\Stdlib\Hydrator\ClassMethods;
class TextMapperFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $serviceLocator) {
return new TextMapper($serviceLocator->get("Zend\Db\Adapter\Adapter"), new Home(), new ClassMethods(false));
}
}
?>
Service for text:
<?php
namespace Site\Service;
use Site\Model\Home;
use Site\Model\HomeInterface;
use Site\Mapper\TextMapperInterface;
class HomeService implements HomeServiceInterface {
protected $textMapper;
public function __construct (TextMapperInterface $textMapper) {
$this->textMapper = $textMapper;
}
public function findText($name) {
return $this->textMapper->find($name);
}
}
?>
Factory for this service:
<?php
namespace Site\Factory;
use Site\Service\HomeService;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class HomeServiceFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $serviceLocator) {
$textMapper = $serviceLocator->get("Site\Mapper\TextMapperInterface");
return new HomeService($textMapper);
}
}
?>
Controller
<?php
namespace Site\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Site\Service\HomeServiceInterface;
use Zend\View\Model\ViewModel;
class SkeletonController extends AbstractActionController {
protected $homeService;
public function __construct(HomeServiceInterface $homeService) {
$this->homeService = $homeService;
}
public function indexAction() {
return new ViewModel(array (
"home" => $this->homeService->findText("home")
));
}
}
?>
Finally, the view:
<?php echo $this->home->getText(); ?>
Code for slider is similar and both the parts of this page are likely having the same problem. As i said, db is detected, tables and columns too, they aren't empty but nothing gets echoed. Interfaces are properly written, defining all the functions. All views are in the Site\view\Site\Skeleton folder. Any clues about where the problem is? Thank you.
Your code looks good. The only issue I can see is that you are using the ClassMethods hydrator and you have no setters on your entity.
The hydrator will use the entity API to hydrate the entity, if the setId, setName or setText are not callable then the values will not be set.
Although I recommend adding the missing methods you can also use the Zend\Stdlib\Hydrator\Reflection to set the entity properties without setters (via SPL ReflectionProperty)

How to replace the Laravel Builder class

I want to replace the Laravels builder class with my own that's extending from it. I thought it would be as simple as matter of App::bind but it seems that does not work. Where should I place the binding and what is the proper way to do that in Laravel?
This is what I have tried:
my Builder:
use Illuminate\Database\Eloquent\Builder as BaseBuilder;
class Builder extends BaseBuilder
{
/**
* Find a model by its primary key.
*
* #param mixed $id
* #param array $columns
* #return \Illuminate\Database\Eloquent\Model|static|null
*/
public function find($id, $columns = array('*'))
{
Event::fire('before.find', array($this));
$result = parent::find($id, $columns);
Event::fire('after.find', array($this));
return $result;
}
}
And next I tried to register the binding in bootstrap/start.php file like this :
$app->bind('Illuminate\\Database\\Eloquent\\Builder', 'MyNameSpace\\Database\\Eloquent\\Builder');
return $app;
Illuminate\Database\Eloquent\Builder class is an internal class and as such it is not dependency injected into the Illuminate\Database\Eloquent\Model class, but kind of hard coded there.
To do what you want to do, I would extend the Illuminate\Database\Eloquent\Model to MyNamespace\Database\Eloquent\Model class and override newEloquentBuilder function.
public function newEloquentBuilder($query)
{
return new MyNamespace\Database\Eloquent\Builder($query);
}
Then alias MyNamespace\Database\Eloquent\Model to Eloquent at the aliases in app/config/app.php
Both of the answers are correct in some way. You have to decide what your goal is.
Change Eloquent Builder
For example, if you want to add a new method only for eloquent models (eg. something like scopes, but maybe a little more advanced so it’s not possible in a scope)
Create a new Class extending the Eloquent Builder, for Example CustomEloquentBuilder.
use Illuminate\Database\Eloquent\Builder;
class CustomEloquentBuilder extends Builder
{
public function myMethod()
{
// some method things
}
}
Create a Custom Model and overwrite the method newEloquentBuilder
use Namespace\Of\CustomEloquentBuilder;
use Illuminate\Database\Eloquent\Model;
class CustomModel extends Model
{
public function newEloquentBuilder($query)
{
return new CustomEloquentBuilder($query);
}
}
Change Database Query Builder
For example to modify the where-clause for all database accesses
Create a new Class extending the Database Builder, for Example CustomQueryBuilder.
use Illuminate\Database\Query\Builder;
class CustomQueryBuilder extends Builder
{
public function myMethod()
{
// some method things
}
}
Create a Custom Model and overwrite the method newBaseQueryBuilder
use Namespace\Of\CustomQueryBuilder;
use Illuminate\Database\Eloquent\Model;
class CustomModel extends Model
{
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new CustomQueryBuilder(
$connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
);
}
}
Laravel Version: 5.5 / this code is untestet
The answer above doesn't exactly work for laravel > 5 so I done some digging and I found this!
https://github.com/laravel/framework/blob/5.2/src/Illuminate/Database/Eloquent/Model.php#L1868
use this instead!
protected function newBaseQueryBuilder()
{
$conn = $this->getConnection();
$grammar = $conn->getQueryGrammar();
return new QueryBuilder($conn, $grammar, $conn->getPostProcessor());
}

Categories