fix cs + read remote calendar based on user access

This commit is contained in:
Julien Fastré 2022-05-09 13:36:59 +02:00
parent 811798e23f
commit 8abce5ab85
17 changed files with 189 additions and 115 deletions

View File

@ -15,7 +15,6 @@ use Chill\CalendarBundle\Synchro\Connector\MSGraph\MachineTokenStorage;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry; use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Question\Question;
use TheNetworg\OAuth2\Client\Provider\Azure; use TheNetworg\OAuth2\Client\Provider\Azure;

View File

@ -1,10 +1,17 @@
<?php <?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Command; namespace Chill\CalendarBundle\Command;
use Chill\CalendarBundle\Synchro\Connector\MSGraph\MachineTokenStorage;
use Chill\CalendarBundle\Synchro\Connector\MSGraph\MapCalendarToUser; use Chill\CalendarBundle\Synchro\Connector\MSGraph\MapCalendarToUser;
use Chill\CalendarBundle\Synchro\Connector\MSGraphRemoteCalendarConnector;
use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -39,7 +46,7 @@ class MapUserCalendarCommand extends Command
foreach ($users as $user) { foreach ($users as $user) {
$this->mapCalendarToUser->writeMetadata($user); $this->mapCalendarToUser->writeMetadata($user);
$offset++; ++$offset;
} }
$this->em->flush(); $this->em->flush();

View File

@ -45,7 +45,7 @@ class RemoteCalendarConnectAzureController
return $this->clientRegistry return $this->clientRegistry
->getClient('azure') // key used in config/packages/knpu_oauth2_client.yaml ->getClient('azure') // key used in config/packages/knpu_oauth2_client.yaml
->redirect([ ->redirect([
'https://graph.microsoft.com/.default' 'https://graph.microsoft.com/.default', 'offline_access',
]); ]);
} }

View File

@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Controller; namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\Synchro\Connector\MSGraph\RemoteEventConverter;
use Chill\CalendarBundle\Synchro\Connector\RemoteCalendarConnectorInterface; use Chill\CalendarBundle\Synchro\Connector\RemoteCalendarConnectorInterface;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use DateTimeImmutable; use DateTimeImmutable;
@ -31,7 +30,6 @@ class RemoteCalendarProxyController
private SerializerInterface $serializer; private SerializerInterface $serializer;
public function __construct(RemoteCalendarConnectorInterface $remoteCalendarConnector, SerializerInterface $serializer) public function __construct(RemoteCalendarConnectorInterface $remoteCalendarConnector, SerializerInterface $serializer)
{ {
$this->remoteCalendarConnector = $remoteCalendarConnector; $this->remoteCalendarConnector = $remoteCalendarConnector;
@ -45,8 +43,9 @@ class RemoteCalendarProxyController
{ {
if ($request->query->has('startDate')) { if ($request->query->has('startDate')) {
$startDate = DateTimeImmutable::createFromFormat('Y-m-d', $request->query->get('startDate')); $startDate = DateTimeImmutable::createFromFormat('Y-m-d', $request->query->get('startDate'));
if (false === $startDate) { if (false === $startDate) {
throw new BadRequestHttpException("startDate on bad format"); throw new BadRequestHttpException('startDate on bad format');
} }
} else { } else {
throw new BadRequestHttpException('startDate not provided'); throw new BadRequestHttpException('startDate not provided');
@ -54,8 +53,9 @@ class RemoteCalendarProxyController
if ($request->query->has('endDate')) { if ($request->query->has('endDate')) {
$endDate = DateTimeImmutable::createFromFormat('Y-m-d', $request->query->get('endDate')); $endDate = DateTimeImmutable::createFromFormat('Y-m-d', $request->query->get('endDate'));
if (false === $endDate) { if (false === $endDate) {
throw new BadRequestHttpException("endDate on bad format"); throw new BadRequestHttpException('endDate on bad format');
} }
} else { } else {
throw new BadRequestHttpException('endDate not provided'); throw new BadRequestHttpException('endDate not provided');

View File

@ -16,12 +16,12 @@ use TheNetworg\OAuth2\Client\Provider\Azure;
class MSGraphClient class MSGraphClient
{ {
private MachineTokenStorage $machineTokenStorage;
private Azure $provider; private Azure $provider;
private MSGraphTokenStorage $tokenStorage; private MSGraphTokenStorage $tokenStorage;
private MachineTokenStorage $machineTokenStorage;
/** /**
* @param mixed $calendar * @param mixed $calendar
* *
@ -38,6 +38,4 @@ class MSGraphClient
return $response; return $response;
} }
} }

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Synchro\Connector\MSGraph; namespace Chill\CalendarBundle\Synchro\Connector\MSGraph;
use LogicException;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use TheNetworg\OAuth2\Client\Provider\Azure; use TheNetworg\OAuth2\Client\Provider\Azure;
use TheNetworg\OAuth2\Client\Token\AccessToken; use TheNetworg\OAuth2\Client\Token\AccessToken;
@ -19,10 +20,10 @@ class MSGraphTokenStorage
{ {
public const MS_GRAPH_ACCESS_TOKEN = 'msgraph_access_token'; public const MS_GRAPH_ACCESS_TOKEN = 'msgraph_access_token';
private SessionInterface $session;
private Azure $azure; private Azure $azure;
private SessionInterface $session;
public function __construct(Azure $azure, SessionInterface $session) public function __construct(Azure $azure, SessionInterface $session)
{ {
$this->azure = $azure; $this->azure = $azure;
@ -35,7 +36,7 @@ class MSGraphTokenStorage
$token = $this->session->get(self::MS_GRAPH_ACCESS_TOKEN, null); $token = $this->session->get(self::MS_GRAPH_ACCESS_TOKEN, null);
if (null === $token) { if (null === $token) {
throw new \LogicException('unexisting token'); throw new LogicException('unexisting token');
} }
if ($token->hasExpired()) { if ($token->hasExpired()) {

View File

@ -1,21 +1,30 @@
<?php <?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Synchro\Connector\MSGraph; namespace Chill\CalendarBundle\Synchro\Connector\MSGraph;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait; use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use LogicException;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface; use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface; use Symfony\Contracts\HttpClient\ResponseStreamInterface;
class MachineHttpClient implements HttpClientInterface class MachineHttpClient implements HttpClientInterface
{ {
use BearerAuthorizationTrait;
private HttpClientInterface $decoratedClient; private HttpClientInterface $decoratedClient;
private MachineTokenStorage $machineTokenStorage; private MachineTokenStorage $machineTokenStorage;
use BearerAuthorizationTrait;
/** /**
* @param HttpClientInterface $decoratedClient * @param HttpClientInterface $decoratedClient
*/ */
@ -37,20 +46,24 @@ class MachineHttpClient implements HttpClientInterface
case 'GET': case 'GET':
case 'HEAD': case 'HEAD':
$options['headers']['Accept'] = 'application/json'; $options['headers']['Accept'] = 'application/json';
break; break;
case 'POST': case 'POST':
case 'PUT': case 'PUT':
case 'PATCH': case 'PATCH':
$options['headers']['Content-Type'] = 'application/json'; $options['headers']['Content-Type'] = 'application/json';
break; break;
default: default:
throw new \LogicException("Method not supported: $method"); throw new LogicException("Method not supported: {$method}");
} }
return $this->decoratedClient->request($method, $url, $options); return $this->decoratedClient->request($method, $url, $options);
} }
public function stream($responses, float $timeout = null): ResponseStreamInterface public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{ {
return $this->decoratedClient->stream($responses, $timeout); return $this->decoratedClient->stream($responses, $timeout);
} }

View File

@ -20,11 +20,11 @@ class MachineTokenStorage
{ {
private const KEY = 'msgraph_access_token'; private const KEY = 'msgraph_access_token';
private ChillRedis $chillRedis; private ?AccessTokenInterface $accessToken = null;
private Azure $azure; private Azure $azure;
private ?AccessTokenInterface $accessToken = null; private ChillRedis $chillRedis;
public function __construct(Azure $azure, ChillRedis $chillRedis) public function __construct(Azure $azure, ChillRedis $chillRedis)
{ {

View File

@ -1,5 +1,14 @@
<?php <?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Synchro\Connector\MSGraph; namespace Chill\CalendarBundle\Synchro\Connector\MSGraph;
use Chill\CalendarBundle\Synchro\Connector\MSGraphRemoteCalendarConnector; use Chill\CalendarBundle\Synchro\Connector\MSGraphRemoteCalendarConnector;
@ -7,31 +16,42 @@ use Chill\MainBundle\Entity\User;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/** /**
* Write metadata to user, which allow to find his default calendar * Write metadata to user, which allow to find his default calendar.
*/ */
class MapCalendarToUser class MapCalendarToUser
{ {
public const METADATA_KEY = 'msgraph'; public const METADATA_KEY = 'msgraph';
private MSGraphRemoteCalendarConnector $remoteCalendarConnector;
private LoggerInterface $logger; private LoggerInterface $logger;
private MSGraphRemoteCalendarConnector $remoteCalendarConnector;
public function __construct(MSGraphRemoteCalendarConnector $remoteCalendarConnector, LoggerInterface $logger) public function __construct(MSGraphRemoteCalendarConnector $remoteCalendarConnector, LoggerInterface $logger)
{ {
$this->remoteCalendarConnector = $remoteCalendarConnector; $this->remoteCalendarConnector = $remoteCalendarConnector;
$this->logger = $logger; $this->logger = $logger;
} }
public function getCalendarId(User $user): ?string
{
if (null === $mskey = ($user->getAttributes()[self::METADATA_KEY] ?? null)) {
return null;
}
return $msKey['defaultCalendarId'] ?? null;
}
public function writeMetadata(User $user): User public function writeMetadata(User $user): User
{ {
if (null === $userData = $this->remoteCalendarConnector->getUserByEmail($user->getEmailCanonical())) { if (null === $userData = $this->remoteCalendarConnector->getUserByEmail($user->getEmailCanonical())) {
$this->logger->warning('[MapCalendarToUser] could find user on msgraph', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]); $this->logger->warning('[MapCalendarToUser] could find user on msgraph', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]);
return $this->writeNullData($user); return $this->writeNullData($user);
} }
if (null === $defaultCalendar = $this->remoteCalendarConnector->getDefaultUserCalendar($userData['id'])) { if (null === $defaultCalendar = $this->remoteCalendarConnector->getDefaultUserCalendar($userData['id'])) {
$this->logger->warning('[MapCalendarToUser] could find default calendar', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]); $this->logger->warning('[MapCalendarToUser] could find default calendar', ['userId' => $user->getId(), 'email' => $user->getEmailCanonical()]);
return $this->writeNullData($user); return $this->writeNullData($user);
} }
@ -46,14 +66,4 @@ class MapCalendarToUser
{ {
return $user->unsetAttribute(self::METADATA_KEY); return $user->unsetAttribute(self::METADATA_KEY);
} }
public function getCalendarId(User $user): ?string
{
if (null === $mskey = ($user->getAttributes()[self::METADATA_KEY] ?? null)) {
return null;
}
return $msKey['defaultCalendarId'] ?? null;
}
} }

View File

@ -1,19 +1,49 @@
<?php <?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Synchro\Connector\MSGraph; namespace Chill\CalendarBundle\Synchro\Connector\MSGraph;
use Chill\CalendarBundle\Synchro\Model\RemoteEvent; use Chill\CalendarBundle\Synchro\Model\RemoteEvent;
use DateTimeImmutable;
use DateTimeZone;
class RemoteEventConverter class RemoteEventConverter
{ {
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';
private DateTimeZone $defaultDateTimeZone;
private DateTimeZone $remoteDateTimeZone;
public function __construct()
{
$this->defaultDateTimeZone = (new DateTimeImmutable())->getTimezone();
$this->remoteDateTimeZone = new DateTimeZone('UTC');
}
public function convertToRemote(array $event): RemoteEvent public function convertToRemote(array $event): RemoteEvent
{ {
$startDate =
DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['start']['dateTime'])
->setTimezone($this->defaultDateTimeZone);
$endDate =
DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['end']['dateTime'], $this->remoteDateTimeZone)
->setTimezone($this->defaultDateTimeZone);
return new RemoteEvent( return new RemoteEvent(
$event['id'], $event['id'],
$event['subject'], $event['subject'],
'', '',
\DateTimeImmutable::createFromFormat('Y-m-dTH:i:s.u', $event['start']['dateTime'], new \DateTimeZone('UTC')), $startDate,
\DateTimeImmutable::createFromFormat('Y-m-dTH:i:s.u', $event['end']['dateTime'], new \DateTimeZone('UTC')) $endDate
); );
} }
} }

View File

@ -1,20 +1,30 @@
<?php <?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Synchro\Connector\MSGraph; namespace Chill\CalendarBundle\Synchro\Connector\MSGraph;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait; use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use LogicException;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface; use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface; use Symfony\Contracts\HttpClient\ResponseStreamInterface;
class UserHttpClient class UserHttpClient
{ {
use BearerAuthorizationTrait;
private HttpClientInterface $decoratedClient; private HttpClientInterface $decoratedClient;
private MSGraphTokenStorage $tokenStorage; private MSGraphTokenStorage $tokenStorage;
use BearerAuthorizationTrait;
/** /**
* @param HttpClientInterface $decoratedClient * @param HttpClientInterface $decoratedClient
*/ */
@ -36,22 +46,25 @@ class UserHttpClient
case 'GET': case 'GET':
case 'HEAD': case 'HEAD':
$options['headers']['Accept'] = 'application/json'; $options['headers']['Accept'] = 'application/json';
break; break;
case 'POST': case 'POST':
case 'PUT': case 'PUT':
case 'PATCH': case 'PATCH':
$options['headers']['Content-Type'] = 'application/json'; $options['headers']['Content-Type'] = 'application/json';
break; break;
default: default:
throw new \LogicException("Method not supported: $method"); throw new LogicException("Method not supported: {$method}");
} }
return $this->decoratedClient->request($method, $url, $options); return $this->decoratedClient->request($method, $url, $options);
} }
public function stream($responses, float $timeout = null): ResponseStreamInterface public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{ {
return $this->decoratedClient->stream($responses, $timeout); return $this->decoratedClient->stream($responses, $timeout);
} }
} }

View File

@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Synchro\Connector; namespace Chill\CalendarBundle\Synchro\Connector;
use Chill\CalendarBundle\Synchro\Connector\MSGraph\MachineHttpClient; use Chill\CalendarBundle\Synchro\Connector\MSGraph\MachineHttpClient;
use Chill\CalendarBundle\Synchro\Connector\MSGraph\MSGraphClient;
use Chill\CalendarBundle\Synchro\Connector\MSGraph\MSGraphTokenStorage; use Chill\CalendarBundle\Synchro\Connector\MSGraph\MSGraphTokenStorage;
use Chill\CalendarBundle\Synchro\Connector\MSGraph\RemoteEventConverter; use Chill\CalendarBundle\Synchro\Connector\MSGraph\RemoteEventConverter;
use Chill\CalendarBundle\Synchro\Connector\MSGraph\UserHttpClient; use Chill\CalendarBundle\Synchro\Connector\MSGraph\UserHttpClient;
@ -21,19 +20,18 @@ use DateTimeImmutable;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use function Amp\Iterator\toArray;
class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
{ {
private MachineHttpClient $machineHttpClient; private MachineHttpClient $machineHttpClient;
private UserHttpClient $userHttpClient; private RemoteEventConverter $remoteEventConverter;
private MSGraphTokenStorage $tokenStorage; private MSGraphTokenStorage $tokenStorage;
private UrlGeneratorInterface $urlGenerator; private UrlGeneratorInterface $urlGenerator;
private RemoteEventConverter $remoteEventConverter; private UserHttpClient $userHttpClient;
public function __construct( public function __construct(
MachineHttpClient $machineHttpClient, MachineHttpClient $machineHttpClient,
@ -49,12 +47,30 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
$this->userHttpClient = $userHttpClient; $this->userHttpClient = $userHttpClient;
} }
public function getDefaultUserCalendar(string $idOrUserPrincipalName): ?array
{
$value = $this->machineHttpClient->request('GET', "users/{$idOrUserPrincipalName}/calendars", [
'query' => ['$filter' => 'isDefaultCalendar eq true'],
])->toArray()['value'];
return $value[0] ?? null;
}
public function getMakeReadyResponse(string $returnPath): Response public function getMakeReadyResponse(string $returnPath): Response
{ {
return new RedirectResponse($this->urlGenerator return new RedirectResponse($this->urlGenerator
->generate('chill_calendar_remote_connect_azure', ['returnPath' => $returnPath])); ->generate('chill_calendar_remote_connect_azure', ['returnPath' => $returnPath]));
} }
public function getUserByEmail(string $email): ?array
{
$value = $this->machineHttpClient->request('GET', 'users', [
'query' => ['$filter' => "mail eq '{$email}'"],
])->toArray()['value'];
return $value[0] ?? null;
}
public function isReady(): bool public function isReady(): bool
{ {
return $this->tokenStorage->hasToken(); return $this->tokenStorage->hasToken();
@ -69,29 +85,11 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
'query' => [ 'query' => [
'startDateTime' => $startDate->format(DateTimeImmutable::ATOM), 'startDateTime' => $startDate->format(DateTimeImmutable::ATOM),
'endDateTime' => $endDate->format(DateTimeImmutable::ATOM), 'endDateTime' => $endDate->format(DateTimeImmutable::ATOM),
'$select' => 'id,subject,start,end' '$select' => 'id,subject,start,end',
] ],
] ]
)->toArray(); )->toArray();
return array_map(function($item) { return $this->remoteEventConverter->convertToRemote($item);}, $bareEvents['value']); return array_map(function ($item) { return $this->remoteEventConverter->convertToRemote($item); }, $bareEvents['value']);
}
public function getUserByEmail(string $email): ?array
{
$value = $this->machineHttpClient->request('GET', 'users', [
'query' => ['$filter' => "mail eq '${email}'"],
])->toArray()['value'];
return $value[0] ?? null;
}
public function getDefaultUserCalendar(string $idOrUserPrincipalName): ?array
{
$value = $this->machineHttpClient->request('GET', "users/$idOrUserPrincipalName/calendars", [
'query' => ['$filter' => 'isDefaultCalendar eq true'],
])->toArray()['value'];
return $value[0] ?? null;
} }
} }

View File

@ -15,7 +15,6 @@ use Chill\CalendarBundle\Synchro\Connector\MSGraphRemoteCalendarConnector;
use Chill\CalendarBundle\Synchro\Connector\NullRemoteCalendarConnector; use Chill\CalendarBundle\Synchro\Connector\NullRemoteCalendarConnector;
use Chill\CalendarBundle\Synchro\Connector\RemoteCalendarConnectorInterface; use Chill\CalendarBundle\Synchro\Connector\RemoteCalendarConnectorInterface;
use RuntimeException; use RuntimeException;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use TheNetworg\OAuth2\Client\Provider\Azure; use TheNetworg\OAuth2\Client\Provider\Azure;

View File

@ -16,7 +16,6 @@ use Symfony\Component\Serializer\Annotation as Serializer;
class RemoteEvent class RemoteEvent
{ {
public string $description; public string $description;
/** /**

View File

@ -361,13 +361,6 @@ class User implements AdvancedUserInterface
return $this; return $this;
} }
public function unsetAttribute($key): self
{
unset($this->attributes[$key]);
return $this;
}
public function setCurrentLocation(?Location $currentLocation): User public function setCurrentLocation(?Location $currentLocation): User
{ {
$this->currentLocation = $currentLocation; $this->currentLocation = $currentLocation;
@ -494,4 +487,11 @@ class User implements AdvancedUserInterface
return $this; return $this;
} }
public function unsetAttribute($key): self
{
unset($this->attributes[$key]);
return $this;
}
} }

View File

@ -44,6 +44,16 @@ final class UserRepository implements ObjectRepository
return $this->countBy(['enabled' => true]); return $this->countBy(['enabled' => true]);
} }
public function countByNotHavingAttribute(string $key): int
{
$rsm = new ResultSetMapping();
$rsm->addScalarResult('count', 'count');
$sql = 'SELECT count(*) FROM users u WHERE NOT attributes ?? :key OR attributes IS NULL AND enabled IS TRUE';
return $this->entityManager->createNativeQuery($sql, $rsm)->setParameter(':key', $key)->getSingleScalarResult();
}
public function countByUsernameOrEmail(string $pattern): int public function countByUsernameOrEmail(string $pattern): int
{ {
$qb = $this->queryByUsernameOrEmail($pattern); $qb = $this->queryByUsernameOrEmail($pattern);
@ -85,6 +95,29 @@ final class UserRepository implements ObjectRepository
return $this->findBy(['enabled' => true], $orderBy, $limit, $offset); return $this->findBy(['enabled' => true], $orderBy, $limit, $offset);
} }
/**
* Find users which does not have a key on attribute column.
*
* @return array|User[]
*/
public function findByNotHavingAttribute(string $key, ?int $limit = null, ?int $offset = null): array
{
$rsm = new ResultSetMappingBuilder($this->entityManager);
$rsm->addRootEntityFromClassMetadata(User::class, 'u');
$sql = 'SELECT ' . $rsm->generateSelectClause() . ' FROM users u WHERE NOT attributes ?? :key OR attributes IS NULL AND enabled IS TRUE';
if (null !== $limit) {
$sql .= " LIMIT {$limit}";
}
if (null !== $offset) {
$sql .= " OFFSET {$offset}";
}
return $this->entityManager->createNativeQuery($sql, $rsm)->setParameter(':key', $key)->getResult();
}
public function findByUsernameOrEmail(string $pattern) public function findByUsernameOrEmail(string $pattern)
{ {
$qb = $this->queryByUsernameOrEmail($pattern); $qb = $this->queryByUsernameOrEmail($pattern);
@ -159,39 +192,6 @@ final class UserRepository implements ObjectRepository
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
/**
* Find users which does not have a key on attribute column
*
* @return array|User[]
*/
public function findByNotHavingAttribute(string $key, ?int $limit = null, ?int $offset = null): array
{
$rsm = new ResultSetMappingBuilder($this->entityManager);
$rsm->addRootEntityFromClassMetadata(User::class, 'u');
$sql = "SELECT ".$rsm->generateSelectClause()." FROM users u WHERE NOT attributes ?? :key OR attributes IS NULL AND enabled IS TRUE";
if (null !== $limit) {
$sql .= " LIMIT $limit";
}
if (null !== $offset) {
$sql .= " OFFSET $offset";
}
return $this->entityManager->createNativeQuery($sql, $rsm)->setParameter(':key', $key)->getResult();
}
public function countByNotHavingAttribute(string $key): int
{
$rsm = new ResultSetMapping();
$rsm->addScalarResult('count', 'count');
$sql = "SELECT count(*) FROM users u WHERE NOT attributes ?? :key OR attributes IS NULL AND enabled IS TRUE";
return $this->entityManager->createNativeQuery($sql, $rsm)->setParameter(':key', $key)->getSingleScalarResult();
}
public function getClassName() public function getClassName()
{ {
return User::class; return User::class;

View File

@ -1,5 +1,12 @@
<?php <?php
/**
* 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.
*/
declare(strict_types=1); declare(strict_types=1);
namespace Chill\Migrations\Main; namespace Chill\Migrations\Main;
@ -9,6 +16,11 @@ use Doctrine\Migrations\AbstractMigration;
final class Version20220506223243 extends AbstractMigration final class Version20220506223243 extends AbstractMigration
{ {
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users ALTER attributes DROP NOT NULL');
}
public function getDescription(): string public function getDescription(): string
{ {
return 'Force user attribute to be an array'; return 'Force user attribute to be an array';
@ -20,9 +32,4 @@ final class Version20220506223243 extends AbstractMigration
$this->addSql('ALTER TABLE users ALTER attributes SET NOT NULL'); $this->addSql('ALTER TABLE users ALTER attributes SET NOT NULL');
$this->addSql('ALTER TABLE users ALTER attributes SET DEFAULT \'{}\'::jsonb'); $this->addSql('ALTER TABLE users ALTER attributes SET DEFAULT \'{}\'::jsonb');
} }
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users ALTER attributes DROP NOT NULL');
}
} }