I have an Entity named "Document" and I use sonata media to handle the file management. So, an Admin can add a new document and attach a file, this document is assigned to an user.
My problem is :
I want the users to be able to download the files that are assigned to them through the "Protected URL"(because if you change the number at the end of the download url, you are able to download files that are assigned to other users)
According to the Sonata Media's doc, I need to create a Download Strategy and the service. I did it, i called it : PrivateDownloadStrategy.php (and made the service)
This file should be based on RolesDownloadStrategy.php, because i want the admins able to download all the files, but how to do for the users be able to download the files that are assigned to them ?
This is my actual PrivateDownloadStrategy.php (copy from rolesDownloadStrategy.php)
<?php
namespace Application\Core\DocumentBundle\Security;
use Sonata\MediaBundle\Security\DownloadStrategyInterface;
use Sonata\MediaBundle\Model\MediaInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class PrivateDownloadStrategy implements DownloadStrategyInterface
{
protected $roles;
protected $security;
protected $translator;
/**
* #param \Symfony\Component\Translation\TranslatorInterface $translator
* #param \Symfony\Component\Security\Core\SecurityContextInterface $security
* #param array $roles
*/
public function __construct(TranslatorInterface $translator, SecurityContextInterface $security, array $roles = array())
{
$this->roles = $roles;
$this->security = $security;
$this->translator = $translator;
}
/**
* #param \Sonata\MediaBundle\Model\MediaInterface $media
* #param \Symfony\Component\HttpFoundation\Request $request
*
* #return bool
*/
public function isGranted(MediaInterface $media, Request $request)
{
return $this->security->getToken() && $this->security->isGranted($this->roles);
}
/**
* #return string
*/
public function getDescription()
{
return $this->translator->trans('description.roles_download_strategy', array('%roles%' => '<code>'.implode('</code>, <code>', $this->roles).'</code>'), 'SonataMediaBundle');
}
}
and the service :
<service id="sonata.media.security.private_strategy" class="Application\Core\DocumentBundle\Security\PrivateDownloadStrategy" >
<argument type="service" id="translator" />
<argument type="service" id="security.context" />
<argument type="collection">
<argument>ROLE_SUPER_ADMIN</argument>
<argument>ROLE_ADMIN</argument>
</argument>
</service>
nb : sonata media doc : link
sonata rolesDownloadStrategy : link
Related
Does anyone knows to modify product data using Shopware\Storefront\Page\Product\ProductPageLoadedEvent ?
services.xml
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="Swag\BasicExample\Service\AddDataToPage" >
<argument type="service" id="product.repository"/>
<tag name="kernel.event_subscriber" />
</service>
</services>
</container>
AddDataToPage.php
<?php declare(strict_types=1);
namespace Swag\BasicExample\Service;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
class AddDataToPage implements EventSubscriberInterface
{
/**
* #var EntityRepositoryInterface
*/
private $productRepository;
/**
* #param EntityRepositoryInterface $productRepository
*/
public function __construct(
EntityRepositoryInterface $productRepository
)
{
$this->productRepository = $productRepository;
}
/**
* #return string[]
*/
public static function getSubscribedEvents(): array
{
return [
ProductPageLoadedEvent::class => 'onProductsLoaded'
];
}
/**
* #param ProductPageLoadedEvent $event
* #return void
*/
public function onProductsLoaded(
ProductPageLoadedEvent $event
)
{
// the product is inside the page object
$productData = $event->getPage()->getProduct();
//modifying name
$this->log($productData->getName());
$productData->setName('Prefix Product Name' . $productData->getName());
$this->log($productData->getName());
//modifying ManufacturerNumber
$this->log($productData->getManufacturerNumber());
$productData->setManufacturerNumber('Prefix ManufacturerNumber' . $productData->getManufacturerNumber());
$this->log($productData->getManufacturerNumber());
$event->getPage()->setProduct($productData);
}
/**
* #param $message
* #return void
*/
private function log($message)
{
$logFileName = 'someFile.log';
file_put_contents(
$logFileName,
$message . PHP_EOL,
FILE_APPEND
);
}
}
After modifying the above mentioned changes it still shows the original data although
$event->getPage()->setProduct($productData);
I'm in doubt whether ProductPageLoadedEvent is an after dispatching event or before dispatching the event.
I'm building a webapp with Symfony and since now I had to repeat a specific pattern for each new controller I built.
For example I have this AdminController :
/**
* #Route("/pro/{uniqid}")
* #ParamConverter("company", options={"mapping":{"uniqid" = "uniqid"}})
* #Security("is_granted(constant('App\\Security\\Voter\\CompanyVoter::VIEW'), company)")
* #package App\Controller
*/
class AdminController extends Controller
{
/**
* #Route("/admin/users/", name="users")
* #return \Symfony\Component\HttpFoundation\Response
*/
public function users(Company $company){}
}
So, each controller must redefine #Route, #ParamConverter and #Security that is extremely redundant.
I tried to create a LoggedController that define every annotation, then make Controller extends that LoggedController, but that does not work.
Is there a solution or should I continue to copy/paste these Annotation each time I create a new Controller that needs to implement it ?
EDIT :
I add the declaration of Company entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\CompanyRepository")
*/
class Company
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
Long story short, you can but it will be a lot easier to duplicate your annotations in every controller.
But if you wan't to do this anyway, here are some solutions.
Routing
This is the easy one. You can define a global prefix in the config/routes/annotations.yaml file.
If you're using the default config, you can try something like this:
# Default controllers
controllers:
resource: ../../src/Controller/
type: annotation
# Company controllers
company_controllers:
resource: ../../src/Controller/Company/
type: annotation
prefix: /pro/{uniqid}
All your routes will now start with /pro/{uniqid} and you can remove the #Route annotation from your controller.
ParamConverter
You can create your own ParamConverter. Everytime you'll use a Company type in an action method, it'll be converted to the matching entity using the uniqid attribute.
Something like this:
// src/ParamConverter/CompanyConverter.php
<?php
namespace App\ParamConverter;
use App\Entity\Company;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Component\HttpFoundation\Request;
class CompanyConverter implements ParamConverterInterface
{
const CONVERTER_ATTRIBUTE = 'uniqid';
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* CompanyConverter constructor.
*
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #inheritdoc
*/
public function apply(Request $request, ParamConverter $configuration)
{
$uniqid = $request->attributes->get(self::CONVERTER_ATTRIBUTE);
$company = $this->entityManager->getRepository(Company::class)->findOneBy(['uniqid' => $uniqid]);
$request->attributes->set($configuration->getName(), $company);
}
/**
* #inheritdoc
*/
function supports(ParamConverter $configuration)
{
return $configuration->getClass() === Company::class;
}
}
With this, you can remove the #ParamConverter annotation from your controller.
Security
You can't use the access_control section of the security.yaml file since custom functions are not yet supported.
Otherwise, something like this could have been nice:
security:
...
access_control:
-
path: ^/pro
allow_if: "is_granted(constant('App\\Security\\Voter\\CompanyVoter::VIEW'), company)"
(Note: It was fixed in Symfony 4.1 but i don't know yet how it will work).
Instead, you can use a subscriber listening on the kernel.request kernel event:
<?php
namespace App\Subscriber;
use App\Entity\Company;
use App\Security\CompanyVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class SecurityListener implements EventSubscriberInterface
{
/**
* #var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* #param AuthorizationCheckerInterface $authorizationChecker
* #param EntityManagerInterface $entityManagerInterface
*/
public function __construct(AuthorizationCheckerInterface $authorizationChecker, EntityManagerInterface $entityManager)
{
$this->authorizationChecker = $authorizationChecker;
$this->entityManager = $entityManager;
}
/**
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$uniqid = $request->attributes->get('uniqid')) {
return;
}
$company = $this->entityManager->getRepository(Company::class)->findOneBy(['titre' => $uniqid]);
if (!$this->authorizationChecker->isGranted(CompanyVoter::VIEW, $company)) {
throw new AccessDeniedHttpException();
}
}
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'onKernelRequest',
);
}
}
So, this is not the first time I am creating the service but I just can't resolve the error
You have requested a non-existent service "global_settings".
Steps I took to ensure service is properly setup:
My AppBundleExtension.php
namespace AppBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader;
class AppBundleExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('settings.xml');
}
}
My settings.xml
<?xml version="1.0" encoding="UTF-8" ?>
<container
xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="global_settings" class="AppBundle\Services\GlobalSettings">
<call method="setEntityManager">
<argument type="service" id="doctrine.orm.default_entity_manager" />
</call>
</service>
</services>
</container>
My GlobalSettings service
namespace AppBundle\Services;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
class GlobalSettings
{
/**
* #var EntityManager
*/
protected $em;
/**
* #var EntityRepository
*/
protected $repo;
public function setEntityManager(EntityManager $em) {
$this->em = $em;
$this->repo = null;
}
/**
* #return array with name => value
*/
public function all() {
return $this->$this->getRepo()->findAll();
}
/**
* #param string $name Name of the setting.
* #return string|null Value of the setting.
* #throws \RuntimeException If the setting is not defined.
*/
public function get($name) {
$setting = $this->$this->getRepo()->findOneBy(array(
'name' => $name,
));
if ($setting === null) {
throw $this->createNotFoundException($name);
}
return $setting->getValue();
}
/**
* #param string $name Name of the setting to update.
* #param string|null $value New value for the setting.
* #throws \RuntimeException If the setting is not defined.
*/
public function set($name, $value) {
$setting = $this->$this->getRepo()->findOneBy(array(
'name' => $name,
));
if ($setting === null) {
throw $this->createNotFoundException($name);
}
$setting->setValue($value);
$this->em->flush($setting);
}
/**
* #return EntityRepository
*/
protected function getRepo() {
if ($this->repo === null) {
$this->repo = $this->em->getRepository('AppBundle:Settings');
}
return $this->repo;
}
/**
* #param string $name Name of the setting.
* #return \RuntimeException
*/
protected function createNotFoundException($name) {
return new \RuntimeException(sprintf('Setting "%s" couldn\'t be found.', $name));
}
}
Then inside my controller I trying to access the service using the following code
$data = $this->get('global_settings')->get('paypal_email');
What am I doing wrong? Any help will be really appreciate as I am out of idea.
The reason why I kept getting this error was that my default setting for services was public: false
So to fix that I needed to set the public property to true for my service
services:
# default configuration for services in *this* file
_defaults:
# automatically injects dependencies in your services
autowire: true
# automatically registers your services as commands, event subscribers, etc.
autoconfigure: true
# this means you cannot fetch services directly from the container via $container->get()
# if you need to do this, you can override this setting on individual services
public: false
my_service:
class: AppBundle\Service\MyService
public: true
You wrote:
Steps I took to ensure service is properly setup
My AppBundleExtension.php
And:
I know AppBundleExtension is not loading, what do I need to do to load it? What am I missing?
So it was clear that the AppBundleExtension class was not loaded.
According to the official documentation you should remove the Bundle in the file name and class name:
The name is equal to the bundle name with the Bundle suffix replaced by Extension (e.g. the Extension class of the AppBundle would be called AppExtension and the one for AcmeHelloBundle would be called AcmeHelloExtension).
You can update your config.yml file:
imports:
- { resource: "#AppBundle/Resources/config/services.yml" }
I've converted a PHP application over to Symfony2 and have yet another structural question...
In my old application I would have entity classes that might act upon other entity classes...for instance, I have a search class and a result class. A function like search->updateSearch() would operate upon the search class, and upon its child result class ($this->result->setFoo('bar'). This is just one example of an entity-related function that doesn't belong in Symfony2's entity class.
From what I can tell it seems like the most symfonyesque method would be to create a service, something along the lines of a searchHelper class, to which I could pass the entity manager, $search, and $result classes, and operate on them there.
Does that sound like the best course of action?
Thank you!
For this scenario I use Model Managers, it's intended to be a business layer ORM agnostic interface for operating with entities. Something like:
<?php
/**
* Group entity manager
*/
class GroupManager
{
/**
* Holds the Doctrine entity manager for database interaction
* #var EntityManager
*/
protected $em;
/**
* Holds the Symfony2 event dispatcher service
* #var EventDispatcherInterface
*/
protected $dispatcher;
/**
* Entity specific repository, useful for finding entities, for example
* #var EntityRepository
*/
protected $repository;
/**
* Constructor
*
* #param EventDispatcherInterface $dispatcher
* #param EntityManager $em
* #param string $class
*/
public function __construct(EventDispatcherInterface $dispatcher, EntityManager $em)
{
$this->dispatcher = $dispatcher;
$this->em = $em;
$this->repository = $em->getRepository($class);
}
/**
* #return Group
*/
public function findGroupBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
}
/**
* #return Group
*/
public function createGroup()
{
$group = new Group();
// Some initialization or creation logic
return $group;
}
/**
* Update a group object
*
* #param Group $group
* #param boolean $andFlush
*/
public function updateGroup(Group $group, $andFlush = true)
{
$this->em->persist($group);
if ($andFlush) {
$this->em->flush();
}
}
/**
* Add a user to a group
*
* #param User $user
* #param Group $group
* #return Membership
*/
public function addUserToGroup(User $user, Group $group)
{
$membership= $this->em->getRepository('GroupBundle:Membership')
->findOneBy(array(
'user' => $user->getId(),
'group' => $group->getId(),
));
if ($membership && $membership->isActive()) {
return null;
} elseif ($membership && !$membership->isActive()) {
$membership->setActive(true);
$this->em->persist($membership);
$this->em->flush();
} else {
$membership = new Membership();
$membership->setUser($user);
$membership->setGroup($group);
$this->em->persist($membership);
$this->em->flush();
}
$this->dispatcher->dispatch(
GroupEvents::USER_JOINED_GROUP, new MembershipEvent($user, $group)
);
return $membership;
}
And then the service definition:
<service id="app.model_manager.group" class="App\GroupBundle\Entity\GroupManager">
<argument type="service" id="event_dispatcher" />
<argument type="service" id="doctrine.orm.entity_manager" />
</service>
You can inject the logger, mailer, router, or whichever other service you could need.
Take a look to FOSUserBundle managers, to get examples and ideas about how to use them.
It sounds like you should be using doctrines custom repository classes. You can check them out here: http://symfony.com/doc/current/book/doctrine.html#custom-repository-classes
Basically they allow you to add custom logic above and beyond your basic entity. Also because they are basically an extension of the entity it makes it really easy to load them in and use their functions:
//Basic Entity File
/**
* #ORM\Entity(repositoryClass="Namespace\Bundle\Repository\ProductRepo")
*/
class Product
{
//...
}
Then the repo file for that entity:
//Basic Repo File
use Doctrine\ORM\EntityRepository;
class ProductRepo extends EntityRepository
{
public function updateSearch($passedParam)
{
// Custom query goes here
}
}
Then from your controller you can load the repo and use the function:
//Controller file
class ProductController extends Controller
{
public function updateSearchAction()
{
$productRepo = $this->getDoctrine()->getManager()->getRepository('Namespace\Bundle\Entity\Product');
// Set $passedParam to what ever it needs to be
$productRepo->updateSearch($passedParam);
}
}
I'm using JMSDiExtraBundle in my Symfony2 project.
Here is my the problem:
Repository.php
abstract class Repository extends DocumentRepository implements ReadOnlyRepositoryInterface {
protected $dm;
protected $repo;
protected $query;
/**
* #InjectParams({
* "dm" = #Inject("doctrine.odm.mongodb.document_manager")
* })
*/
public function __construct(DocumentManager $dm) {
$this->dm = $dm;
parent::__construct($dm, $this->dm->getUnitOfWork(), new ClassMetadata($this->getDocumentName()));
$this->query = $this->dm->createQueryBuilder($this->getDocumentName());
}
}
PostRepository.php
/**
* #Service("post_repository")
*/
class PostRepository extends Repository implements PostRepositoryInterface {
private $uploader;
/**
* #InjectParams({
* "dm" = #Inject("doctrine.odm.mongodb.document_manager"),
* "uploader" = #Inject("uploader"),
* })
*/
public function __construct(DocumentManager $dm, UploaderService $uploader) {
parent::__construct($dm);
$this->uploader = $uploader;
}
}
As can be seen, PostRepository requires 2 dependency : DocumentManager (later injected to Repository as parent) and Uploader.
But it seems that Symfony does something which making it assumed that PostRepository needed 3 dependency : DocumentManager, DocumentManager (again) and Uploader, which off course gives an error since I explicitly stated that the second parameter is required to be an Uploader instance.
Here's from appDevDebugProjectContainer.xml :
<service id="post_repository" class="BusinessLounge\BlogBundle\Repository\PostRepository">
<argument type="service" id="doctrine_mongodb.odm.default_document_manager"/>
<argument type="service" id="doctrine_mongodb.odm.default_document_manager"/>
<argument type="service" id="uploader"/>
</service>
and appDevDebugProjectContainer.php :
/**
* Gets the 'post_repository' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* #return BusinessLounge\BlogBundle\Repository\PostRepository A BusinessLounge\BlogBundle\Repository\PostRepository instance.
*/
protected function getPostRepositoryService()
{
$a = $this->get('doctrine_mongodb.odm.default_document_manager');
return $this->services['post_repository'] = new \BusinessLounge\BlogBundle\Repository\PostRepository($a, $a, $this->get('uploader'));
}
Is this an intended behavior? Or a bug perhaps? Or I did something wrong?
Need advice!
You can just remove the #InjectParams on your abstract parent class, since it is never instantiated anyways. Then only the things you need are injected in your real service.