711 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Export;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\Export\ExportType;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Doctrine\ORM\QueryBuilder;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* Collects all agregators, filters and export from
* the installed bundle, and performs the export logic.
*/
class ExportManager
{
/**
* The collected aggregators, injected by DI.
*
* @var array|AggregatorInterface[]
*/
private array $aggregators = [];
/**
* Collected Exports, injected by DI.
*
* @var array|ExportInterface[]
*/
private array $exports = [];
/**
* The collected filters, injected by DI.
*
* @var array|FilterInterface[]
*/
private array $filters = [];
/**
* Collected Formatters, injected by DI.
*
* @var array|FormatterInterface[]
*/
private array $formatters = [];
public function __construct(
private readonly LoggerInterface $logger,
private readonly AuthorizationCheckerInterface $authorizationChecker,
private readonly AuthorizationHelperInterface $authorizationHelper,
private readonly TokenStorageInterface $tokenStorage,
iterable $exports,
iterable $aggregators,
iterable $filters,
// iterable $formatters,
// iterable $exportElementProvider
) {
$this->exports = iterator_to_array($exports);
$this->aggregators = iterator_to_array($aggregators);
$this->filters = iterator_to_array($filters);
// NOTE: PHP crashes on the next line (exit error code 11). This is desactivated until further investigation
// $this->formatters = iterator_to_array($formatters);
// foreach ($exportElementProvider as $prefix => $provider) {
// $this->addExportElementsProvider($provider, $prefix);
// }
}
/**
* Return a \Generator containing filter which support type. If `$centers` is
* not null, restrict the given filters to the center the user have access to.
*
* if $centers is null, the function will returns all filters where the user
* has access in every centers he can reach (if the user can use the filter F in
* center A, but not in center B, the filter F will not be returned)
*
* @param \Chill\MainBundle\Entity\Center[] $centers the centers where the user have access to
*
* @return FilterInterface[] a \Generator that contains filters. The key is the filter's alias
*/
public function getFiltersApplyingOn(DirectExportInterface|ExportInterface $export, ?array $centers = null): array
{
if ($export instanceof DirectExportInterface) {
return [];
}
$filters = [];
foreach ($this->filters as $alias => $filter) {
if (
\in_array($filter->applyOn(), $export->supportsModifiers(), true)
&& $this->isGrantedForElement($filter, $export, $centers)
) {
$filters[$alias] = $filter;
}
}
return $filters;
}
/**
* Return a \Generator containing aggregators supported by the given export.
*
* @internal This class check the interface implemented by export, and, if ´ListInterface´ is used, return an empty array
*
* @return array<string, AggregatorInterface> an array that contains aggregators. The key is the filter's alias
*/
public function getAggregatorsApplyingOn(DirectExportInterface|ExportInterface $export, ?array $centers = null): array
{
if ($export instanceof ListInterface || $export instanceof DirectExportInterface) {
return [];
}
$aggregators = [];
foreach ($this->aggregators as $alias => $aggregator) {
if (
\in_array($aggregator->applyOn(), $export->supportsModifiers(), true)
&& $this->isGrantedForElement($aggregator, $export, $centers)
) {
$aggregators[$alias] = $aggregator;
}
}
return $aggregators;
}
public function addExportElementsProvider(ExportElementsProviderInterface $provider, string $prefix): void
{
foreach ($provider->getExportElements() as $suffix => $element) {
$alias = $prefix.'_'.$suffix;
if ($element instanceof ExportInterface) {
$this->exports[$alias] = $element;
} elseif ($element instanceof FilterInterface) {
$this->filters[$alias] = $element;
} elseif ($element instanceof AggregatorInterface) {
$this->aggregators[$alias] = $element;
} elseif ($element instanceof FormatterInterface) {
$this->addFormatter($element, $alias);
} else {
throw new \LogicException('This element '.$element::class.' is not an instance of export element');
}
}
}
/**
* add a formatter.
*
* @internal used by DI
*/
public function addFormatter(FormatterInterface $formatter, string $alias)
{
$this->formatters[$alias] = $formatter;
}
public function generateExport(string $exportAlias, array $pickedCentersData, array $data, array $formatterData, User $byUser): FormattedExportGeneration
{
$export = $this->getExport($exportAlias);
$centers = $this->getPickedCenters($pickedCentersData);
$context = new ExportGenerationContext($byUser);
if ($export instanceof DirectExportInterface) {
$generatedExport = $export->generate(
$this->buildCenterReachableScopes($centers, $export),
$data[ExportType::EXPORT_KEY],
);
if ($generatedExport instanceof Response) {
trigger_deprecation('chill-project/chill-bundles', '3.10', 'DirectExportInterface should not return a %s instance, but a %s instance', Response::class, FormattedExportGeneration::class);
return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('Content-Type'));
}
return $generatedExport;
}
$query = $export->initiateQuery(
$this->retrieveUsedModifiers($data),
$this->buildCenterReachableScopes($centers, $export),
$export->denormalizeFormData($data[ExportType::EXPORT_KEY], $context),
);
if ($query instanceof \Doctrine\ORM\NativeQuery) {
// throw an error if the export require other modifier, which is
// not allowed when the export return a `NativeQuery`
if (\count($export->supportsModifiers()) > 0) {
throw new \LogicException("The export with alias `{$exportAlias}` return ".'a `\Doctrine\ORM\NativeQuery` and supports modifiers, which is not allowed. Either the method `supportsModifiers` should return an empty array, or return a `Doctrine\ORM\QueryBuilder`');
}
} elseif ($query instanceof QueryBuilder) {
// handle filters
$this->handleFilters($export, $query, $data[ExportType::FILTER_KEY], $centers, $context);
// handle aggregators
$this->handleAggregators($export, $query, $data[ExportType::AGGREGATOR_KEY], $centers, $context);
$this->logger->notice('[export] will execute this qb in export', [
'dql' => $query->getDQL(),
]);
} else {
throw new \UnexpectedValueException('The method `intiateQuery` should return a `\Doctrine\ORM\NativeQuery` or a `Doctrine\ORM\QueryBuilder` object.');
}
$result = $export->getResult($query, $export->denormalizeFormData($data[ExportType::EXPORT_KEY], $context));
if (!is_iterable($result)) {
throw new \UnexpectedValueException(sprintf('The result of the export should be an iterable, %s given', \gettype($result)));
}
/** @var FormatterInterface $formatter */
$formatter = $this->getFormatter($this->getFormatterAlias($data));
$filtersData = [];
$aggregatorsData = [];
if ($query instanceof QueryBuilder) {
$aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]);
foreach ($aggregators as $alias => $aggregator) {
$aggregatorsData[$alias] = $data[ExportType::AGGREGATOR_KEY][$alias]['form'];
}
}
$filters = $this->retrieveUsedFilters($data[ExportType::FILTER_KEY]);
foreach ($filters as $alias => $filter) {
$filtersData[$alias] = $data[ExportType::FILTER_KEY][$alias]['form'];
}
if (method_exists($formatter, 'generate')) {
return $formatter->generate(
$result,
$formatterData,
$exportAlias,
$data[ExportType::EXPORT_KEY],
$filtersData,
$aggregatorsData,
);
}
trigger_deprecation('chill-project/chill-bundles', '3.10', '%s should implements the "generate" method', FormatterInterface::class);
$generatedExport = $formatter->getResponse(
$result,
$formatterData,
$exportAlias,
$data[ExportType::EXPORT_KEY],
$filtersData,
$aggregatorsData,
);
return new FormattedExportGeneration($generatedExport->getContent(), $generatedExport->headers->get('content-type'));
}
/**
* Generate a response which contains the requested data.
*/
public function generate(string $exportAlias, array $pickedCentersData, array $data, array $formatterData): Response
{
$generated = $this->generateExport(
$exportAlias,
$pickedCentersData,
$data,
$formatterData,
);
return new Response($generated->content, headers: ['Content-Type' => $generated->contentType]);
}
/**
* @param string $alias
*
* @return AggregatorInterface
*
* @throws \RuntimeException if the aggregator is not known
*/
public function getAggregator($alias)
{
if (!\array_key_exists($alias, $this->aggregators)) {
throw new \RuntimeException("The aggregator with alias {$alias} is not known.");
}
return $this->aggregators[$alias];
}
/**
* @return iterable<string, AggregatorInterface>
*/
public function getAggregators(array $aliases): iterable
{
foreach ($aliases as $alias) {
yield $alias => $this->getAggregator($alias);
}
}
/**
* Get the types for known exports.
*
* @return list<string> the existing type for known exports
*/
public function getExistingExportsTypes(): array
{
$existingTypes = [];
foreach ($this->exports as $export) {
if (!\in_array($export->getType(), $existingTypes, true)) {
$existingTypes[] = $export->getType();
}
}
return $existingTypes;
}
/**
* Return an export by his alias.
*
* @param string $alias
*
* @throws \RuntimeException
*/
public function getExport($alias): DirectExportInterface|ExportInterface
{
if (!\array_key_exists($alias, $this->exports)) {
throw new \RuntimeException("The export with alias {$alias} is not known.");
}
return $this->exports[$alias];
}
/**
* Return all exports. The exports's alias are the array's keys.
*
* @param bool $whereUserIsGranted if true (default), restrict to user which are granted the right to execute the export
*
* @return iterable<string, ExportInterface|DirectExportInterface> an array where export's alias are keys
*/
public function getExports($whereUserIsGranted = true): iterable
{
foreach ($this->exports as $alias => $export) {
if ($whereUserIsGranted) {
if ($this->isGrantedForElement($export, null, null)) {
yield $alias => $export;
}
} else {
yield $alias => $export;
}
}
}
/**
* Get all exports grouped in an array.
*
* @return array<string, array<string, ExportInterface|DirectExportInterface>> where keys are the groups's name and value is an array of exports
*/
public function getExportsGrouped(bool $whereUserIsGranted = true): array
{
$groups = ['_' => []];
foreach ($this->getExports($whereUserIsGranted) as $alias => $export) {
if ($export instanceof GroupedExportInterface) {
$groups[$export->getGroup()][$alias] = $export;
} else {
$groups['_'][$alias] = $export;
}
}
return $groups;
}
/**
* @throws \RuntimeException if the filter is not known
*/
public function getFilter(string $alias): FilterInterface
{
if (!\array_key_exists($alias, $this->filters)) {
throw new \RuntimeException("The filter with alias {$alias} is not known.");
}
return $this->filters[$alias];
}
public function getAllFilters(): array
{
$filters = [];
foreach ($this->filters as $alias => $filter) {
$filters[$alias] = $filter;
}
return $filters;
}
/**
* get all filters.
*
* @param array<string> $aliases
*
* @return iterable<string, FilterInterface> $aliases
*/
public function getFilters(array $aliases): iterable
{
foreach ($aliases as $alias) {
yield $alias => $this->getFilter($alias);
}
}
public function getFormatter(string $alias): FormatterInterface
{
if (!\array_key_exists($alias, $this->formatters)) {
throw new \RuntimeException("The formatter with alias {$alias} is not known.");
}
return $this->formatters[$alias];
}
/**
* get the formatter alias from the form export data.
*
* @param array $data the data from the export form
*
* @string the formatter alias|null
*/
public function getFormatterAlias(array $data): ?string
{
if (\array_key_exists(ExportType::PICK_FORMATTER_KEY, $data)) {
return $data[ExportType::PICK_FORMATTER_KEY]['alias'];
}
return null;
}
/**
* Get all formatters which supports one of the given types.
*
* @return iterable<string, FormatterInterface>
*/
public function getFormattersByTypes(array $types): iterable
{
foreach ($this->formatters as $alias => $formatter) {
if (\in_array($formatter->getType(), $types, true)) {
yield $alias => $formatter;
}
}
}
/**
* Get the Center picked by the user for this export. The data are
* extracted from the PickCenterType data.
*
* @param array $data the data from a PickCenterType
*
* @return \Chill\MainBundle\Entity\Center[] the picked center
*/
public function getPickedCenters(array $data): array
{
return $data;
}
/**
* get the aggregators typse used in the form export data.
*
* @param array $data the data from the export form
*
* @return list<string>
*/
public function getUsedAggregatorsAliases(array $data): array
{
$aggregators = $this->retrieveUsedAggregators($data[ExportType::AGGREGATOR_KEY]);
return array_keys(iterator_to_array($aggregators));
}
/**
* Return true if the current user has access to the ExportElement for every
* center, false if the user hasn't access to element for at least one center.
*/
public function isGrantedForElement(
DirectExportInterface|ExportInterface|ModifierInterface $element,
DirectExportInterface|ExportInterface|null $export = null,
?array $centers = null,
): bool {
dump(__METHOD__, $this->tokenStorage->getToken()->getUser());
if ($element instanceof ExportInterface || $element instanceof DirectExportInterface) {
$role = $element->requiredRole();
} else {
if (null === $element->addRole()) {
if (null === $export) {
throw new \LogicException('The export should not be null: as the ModifierInstance element is not an export, we should be aware of the export to determine which role is required');
}
$role = $export->requiredRole();
} else {
$role = $element->addRole();
}
}
if (null === $centers || [] === $centers) {
// we want to try if at least one center is reachable
return [] !== $this->authorizationHelper->getReachableCenters(
$this->tokenStorage->getToken()->getUser(),
$role
);
}
dump($centers);
foreach ($centers as $center) {
if (false === $this->authorizationChecker->isGranted($role, $center)) {
// debugging
$this->logger->debug('user has no access to element', [
'method' => __METHOD__,
'type' => $element::class,
'center' => $center->getName(),
'role' => $role,
]);
return false;
}
}
return true;
}
/**
* build the array required for defining centers and circles in the initiate
* queries of ExportElementsInterfaces.
*
* @param \Chill\MainBundle\Entity\Center[] $centers
*/
private function buildCenterReachableScopes(array $centers, ExportElementInterface $element)
{
$r = [];
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof User) {
return [];
}
foreach ($centers as $center) {
$r[] = [
'center' => $center,
'circles' => $this->authorizationHelper->getReachableScopes(
$user,
$element->requiredRole(),
$center
),
];
}
return $r;
}
/**
* Alter the query with selected aggregators.
*
* Check for acl. If an user is not authorized to see an aggregator, throw an
* UnauthorizedException.
*
* @throw UnauthorizedHttpException if the user is not authorized
*/
private function handleAggregators(
ExportInterface $export,
QueryBuilder $qb,
array $data,
array $center,
ExportGenerationContext $context,
) {
$aggregators = $this->retrieveUsedAggregators($data);
foreach ($aggregators as $alias => $aggregator) {
$formData = $data[$alias];
$aggregator->alterQuery($qb, $aggregator->denormalizeFormData($formData['form'], $context));
}
}
/**
* alter the query with selected filters.
*
* This function check the acl.
*
* @param \Chill\MainBundle\Entity\Center[] $centers the picked centers
*
* @throw UnauthorizedHttpException if the user is not authorized
*/
private function handleFilters(
ExportInterface $export,
QueryBuilder $qb,
mixed $data,
array $centers,
ExportGenerationContext $context,
) {
$filters = $this->retrieveUsedFilters($data);
foreach ($filters as $alias => $filter) {
$formData = $data[$alias];
$this->logger->debug('alter query by filter '.$alias, [
'class' => self::class, 'function' => __FUNCTION__,
]);
$filter->alterQuery($qb, $filter->denormalizeFormData($formData['form'], $context));
}
}
/**
* @return iterable<string, AggregatorInterface>
*/
private function retrieveUsedAggregators(mixed $data): iterable
{
if (null === $data) {
return [];
}
foreach ($data as $alias => $aggregatorData) {
if (true === $aggregatorData['enabled']) {
yield $alias => $this->getAggregator($alias);
}
}
}
/**
* @return string[]
*/
private function retrieveUsedAggregatorsType(mixed $data)
{
if (null === $data) {
return [];
}
$usedTypes = [];
foreach ($this->retrieveUsedAggregators($data) as $alias => $aggregator) {
if (!\in_array($aggregator->applyOn(), $usedTypes, true)) {
$usedTypes[] = $aggregator->applyOn();
}
}
return $usedTypes;
}
private function retrieveUsedFilters(mixed $data): iterable
{
if (null === $data) {
return [];
}
foreach ($data as $alias => $filterData) {
if (true === $filterData['enabled']) {
yield $alias => $this->getFilter($alias);
}
}
}
/**
* Retrieve the filter used in this export.
*
* @return array an array with types
*/
private function retrieveUsedFiltersType(mixed $data): iterable
{
if (null === $data) {
return [];
}
$usedTypes = [];
foreach ($data as $alias => $filterData) {
if (true === $filterData['enabled']) {
$filter = $this->getFilter($alias);
if (!\in_array($filter->applyOn(), $usedTypes, true)) {
$usedTypes[] = $filter->applyOn();
}
}
}
return $usedTypes;
}
/**
* parse the data to retrieve the used filters and aggregators.
*
* @return string[]
*/
private function retrieveUsedModifiers(mixed $data)
{
if (null === $data) {
return [];
}
$usedTypes = array_merge(
$this->retrieveUsedFiltersType($data[ExportType::FILTER_KEY]),
$this->retrieveUsedAggregatorsType($data[ExportType::AGGREGATOR_KEY])
);
$this->logger->debug(
'Required types are '.implode(', ', $usedTypes),
['class' => self::class, 'function' => __FUNCTION__]
);
return array_unique($usedTypes);
}
}