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.
Related
interface UserRepositoryInterface {
public function getUser($userType, $login): Builder;
}
class UserRepository implements UserRepositoryInterface {
public function getUser('clientA', 'user-login', EmailValidatorInterface $emailValidator, PhoneValidatorInterface $phoneValidator): Builder {}
}
Method UserRepository#getUser throws an error because he is not abiding by the contract UserRepositoryInterface#getUser. How can I remove an error, while using method injection?
There are several ways how to solve this kind of error. I 'm not a big fan of overriding the signature of an interface provided method. I prefer DI over the constructor of a class.
Dependency Injection with the constructor
This one makes more sence in my eyes, because it is cleaner. Before overriding interface method signatures you 'll always have to check if the additional parameters are used in more than that one method. If this is the case, just inject the dependency over the constructor.
<?php
declare(strict_types=1);
namespace Marcel;
class UserRepository implements UserRepositoryInterface
{
protected EmailValidatorInterface $emailValidator;
protected PhoneValidatorInterface $phoneValidator;
public function __construct(
EmailValidatorInterface $emailValidator,
PhoneValidatorInterface $phoneValidator
) {
$this->emailValidator = $emailValidator;
$this->phoneValidator = $phoneValidator;
}
public function getUser(string $userType, string $login): Builder
{
// $this->emailValidator and $this->phoneValidator are available
}
}
As you can see this one is exact what the interface implements. No additional parameters. Just clean and simple dependency injection. This type of injection needs a simple factory or something else, that initializes the dependencies.
Extending an interface implemented method
If you can not use dependency injection - for whatever reason - you can extend the method signature from the interface by providing a default value for each additional parameter.
<?php
declare(strict_types=1);
namespace Marcel;
class UserRepository implements UserRepositoryInterface
{
public function getUser(
string $userType,
string $login,
?EmailValidatorInterface $emailValidator = null,
?PhoneValidatorInterface $phoneValidator = null
): Builder
{
// $emailValidator and $phoneValidator are available
// just validate them against null
}
}
This is ugly af but it will work. What happens, when you check if a class implements the UserRepositoryInterface interface? It just secures, that there is a method getUser, which takes two parameters. The interface knows nothing about any other parameters. This is inconsistency in its purest form.
In most DI systems you’d solve this in the constructor instead.
interface UserRepositoryInterface {
public function getUser($userType, $login): Builder;
}
class UserRepository implements UserRepositoryInterface {
public function __construct(
public readonly EmailValidatorInterface $emailValidator,
public readonly PhoneValidatorInterface $phoneValidator
){}
public function getUser('clientA', 'user-login'): Builder {}
}
I have Symfony bundle called upload images:
I want to use parameters in my bundle in my class.
This is my parameter file:
upload-images:
image:
crop_size: 300
My files:
Configuration.php
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('upload-images');
$treeBuilder->getRootNode()
->children()
->arrayNode('image')
->children()
->integerNode('save_original')->end()
->scalarNode('crop_size')->end()
->end()
->end() // twitter
->end();
return $treeBuilder;
}
}
UploadImagesExtension.php
class UploadImagesExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources'));
$loader->load('services.yaml');
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
}
}
And final my service class:
Rotate.php
And in this class I want the parameter: crop_size
I tried the ParameterBagInterface:
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class Rotate
{
private $params;
public function __construct(ParameterBagInterface $params)
{
$this->params = $params;
}
public function Rotate()
{
$cropSize = $params->get('crop_size');
}
}
UserController.php
use verzeilberg\UploadImagesBundle\Service\Rotate;
class UserController extends AbstractController
{
/** #var UserProfileService */
private $service;
private $userService;
public function __construct(
UserProfileService $service,
UserService $userService
) {
$this->service = $service;
$this->userService = $userService;
}
/**
* #param UserInterface $user
* #return Response
*/
public function profile(UserInterface $user)
{
$rotate = new Rotate();
$rotate->Rotate();
.....
...
}
Getting this error:
Too few arguments to function verzeilberg\UploadImagesBundle\Service\Rotate::__construct(), 0 passed in /home/vagrant/projects/diabetigraph-dev/src/Controller/User/UserController.php on line 62 and exactly 1 expected
I have search for a solution. But did not came accross the right one.
According to the latest edit, the error is pretty obvious: if you want to use dependency injection, you have to use it. Calling $rotate = new Rotate(); without any constructor parameters will fail, as Symfony cannot inject them for you.
Instead, inject it through the action:
public function profile(UserInterface $user, Rotate $rotate)
... this will use Symfony's container and inject the ParameterBagInterface, if you have enabled autowiring. If not, you have to write the proper service definitions to get this done
The error you are getting is not directly related to your question.
Either it is an autowiring issue, or maybe you are trying to instanciate the service manually? Please share what you are doing here: UserController.php on line 62
Anyway, to answer your question:
To access the parameters from the parameter bag you will have to set them in the extension.
$container->setParameter('my_bundle.config', $config);
Also, injecting the whole ParameterBag is fine for a project, but should be avoided for a bundle.
Use the DI config to inject just your parameter, OR make your extension implement CompilerPassInterface, and override the definition there. (It may be overkill for such a simple task)
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
I have create a controller that creates a Owner record into database. Everything was done on the CreateOwnerController like this and working properly:
class CreateOwnerController extends Controller
{
public function executeAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$owner = new Owner($request->request->get("name"));
$em->persist($owner);
$em->flush();
return new Response('Added',200);
}
}
Now,In order to refactor that I have created an interface that defines the OwnerRepository:
interface OwnerRepositoryInterface {
public function save(Owner $owner);
}
And a OwnerRepository that implements this interface:
class OwnerRepository extends EntityRepository implements OwnerRepositoryInterface {
public function save(Owner $owner) {
$this->_em->persist($owner);
$this->_em->flush();
}
}
Then I have Created for the application layer a CreateOwnerUseCase Class that receives a OwnerRepository and executes a method to save in into OwnerRepository:
class CreateOwnerUseCase {
private $ownerRepository;
public function __construct(OwnerRepositoryInterface $ownerRepository) {
$this->ownerRepository = $ownerRepository;
}
public function execute(string $ownerName) {
$owner = new Owner($ownerName);
$this->ownerRepository->save($owner);
}
}
Ok, i'm spliting the initial Controller intro layer Domain / Aplication / Framework layers.
On the CreateOwnerController now i have instantiated that Use Case and passed as parameter the OwnerRepository like this:
class CreateOwnerController extends Controller {
public function executeAction(Request $request) {
$createOwnerUseCase = new CreateOwnerUseCase(new OwnerRepository());
$createOwnerUseCase->execute($request->request->get("name"));
return new Response('Added',200);
}
}
But it fails when Make the request to create new Owner:
Warning: Missing argument 1 for Doctrine\ORM\EntityRepository::__construct(), called in /ansible/phpexercises/Frameworks/mpweb-frameworks-symfony/src/MyApp/Bundle/AppBundle/Controller/CreateOwnerController.php
It happens on OwnerRepository passed as parameter. It wants an $em and Mapped Class... What is the meaning of this mapped Class? How solve this error?
This answer is for Symfony 3.3+/4+.
You need to register your repository as a service. Instead of extending it 3rd party code, you should use composition over inheritance.
final class OwnerRepository implements OwnerRepositoryInterface
{
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function save(Owner $owner)
{
$this->entityManager->persist($owner);
$this->entityManager->flush();
}
}
And register it as a service:
# app/config/services.yml
services:
App\Repository\:
# for location app/Repository
resource: ../Repository
You might need to tune paths a bit, to make that work.
To get more extended answer, see How to use Repository with Doctrine as Service in Symfony
How to get translator in model?
Inside view we can get translator using this code
$this->translate('Text')
Inside controller we can get translator using this code
$translator=$this->getServiceLocator()->get('translator');
$translator->translate("Text") ;
But how to get translator in model?
I'd tried so many ways to get service locator in models
2 of those
1)Using MVC events
$e=new MvcEvent();
$sm=$e->getApplication()->getServiceManager();
$this->translator = $sm->get('translator');
if i pring $sm it is showing null. but it works fine in Model.php onBootstrap
2)Created one model which implements ServiceLocatorAwareInterface
SomeModel.php
<?php
namespace Web\Model;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class SomeModel implements ServiceLocatorAwareInterface
{
protected $services;
public function setServiceLocator(ServiceLocatorInterface $locator)
{
$this->services = $locator;
}
public function getServiceLocator()
{
return $this->services;
}
}
and used that inside my model
$sl = new SomeModel();
$sm=$sl->getServiceManager();
var_dump($sm); exit;
$this->translator = $sm->get('translator');
this is also printing null.
If you don't need the servicemanager instance in your model, simply inject translator instance to it.
For example:
// Your model's constructor
class MyModel {
// Use the trait if your php version >= 5.4.0
use \Zend\I18n\Translator\TranslatorAwareTrait;
public function __construct( $translator )
{
$this->setTranslator( $translator );
}
public function modelMethodWhichNeedsToUseTranslator()
{
// ...
$text = $this->getTranslator()->translate('lorem ipsum');
// ...
}
}
When you creating your model first time on service or controller level
class someClass implements ServiceLocatorAwareInterface {
public function theMethodWhichCreatesYourModelInstance()
{
// ...
$sm = $this->getServiceLocator();
$model = new \Namespace\MyModel( $sm->get('translator') )
// ...
}
}
If you need to instantiate your model (new MyModel();) on multiple methods/classes, consider to writing a factory for it.
Here is a nice article about Dependency Injection and PHP by Ralph Schindler for more detailed comments about this approach.
For your Model class to be ServiceLocatorAware, you not only need to implement the interface, you also need to make your model a service of the service manager, and fetch the model from there.
Add your model to the service manager, since it doesn't appear to need any constructor params, it's invokable, so you can add it to the invokables array in service manager config. You can do that by using the getServiceConfig() method in your Module class...
class Module
{
public function getServiceConfig()
{
return array(
'invokables' => array(
'SomeModel' => 'Fully\Qualified\ClassName\To\SomeModel',
),
);
}
}
Then, instead of calling the new keyword to create your model instance, you fetch it from the service manager, for instance, by calling the getServiceLocator() method in a controller action...
public function fooAction()
{
$sm = $this->getServiceLocator();
$model = $sm->get('SomeModel');
}
When your model is fetched from the service manager, a service initializer will look to see if it implements the ServiceLocatorAwareInterface and automatically call setServiceLocator() if it does, passing it an instance of the service manager.