['webhook:read'], 'swagger_definition_name' => 'Read', 'api_included' => ['category'], ], denormalizationContext: [ 'groups' => ['webhook:write'], 'swagger_definition_name' => 'Write', ] )] class Webhook extends FormEntity implements SkipModifiedInterface { public const LOGS_DISPLAY_LIMIT = 100; /** * @var ?int */ #[Groups(['webhook:read'])] private $id; /** * @var ?string */ #[Groups(['webhook:read', 'webhook:write'])] private $name; /** * @var string|null */ #[Groups(['webhook:read', 'webhook:write'])] private $description; /** * @var ?string */ #[Groups(['webhook:read', 'webhook:write'])] private $webhookUrl; /** * @var ?string */ #[Groups(['webhook:read', 'webhook:write'])] private $secret; /** * @var Category|null **/ #[Groups(['webhook:read', 'webhook:write'])] private $category; /** * @var Collection */ #[Groups(['webhook:read', 'webhook:write'])] private $events; /** * @var ArrayCollection */ private $logs; /** * @var array */ private $removedEvents = []; /** * @var mixed[] */ #[Groups(['webhook:read', 'webhook:write'])] private $payload; /** * Holds a simplified array of events, just an array of event types. * It's used for API serializaiton. * * @var array */ #[Groups(['webhook:read', 'webhook:write'])] private $triggers = []; /** * ASC or DESC order for fetching order of the events when queue mode is on. * Null means use the global default. * * @var string|null */ #[Groups(['webhook:read', 'webhook:write'])] private $eventsOrderbyDir; private ?\DateTimeImmutable $markedUnhealthyAt = null; private ?\DateTimeImmutable $unHealthySince = null; private ?\DateTimeImmutable $lastNotificationSentAt = null; public function __construct() { $this->events = new ArrayCollection(); $this->logs = new ArrayCollection(); } public static function loadMetadata(ORM\ClassMetadata $metadata): void { $builder = new ClassMetadataBuilder($metadata); $builder->setTable('webhooks') ->setCustomRepositoryClass(WebhookRepository::class); $builder->addIdColumns(); $builder->addCategory(); $builder->createOneToMany('events', 'Event') ->orphanRemoval() ->setIndexBy('eventType') ->mappedBy('webhook') ->cascadePersist() ->cascadeMerge() ->cascadeDetach() ->build(); $builder->createOneToMany('logs', 'Log')->setOrderBy(['dateAdded' => Order::Descending->value]) ->fetchExtraLazy() ->mappedBy('webhook') ->cascadePersist() ->cascadeMerge() ->cascadeDetach() ->build(); $builder->addNamedField('webhookUrl', Types::TEXT, 'webhook_url'); $builder->addField('secret', Types::STRING); $builder->addNullableField('eventsOrderbyDir', Types::STRING, 'events_orderby_dir'); $builder->addNullableField('markedUnhealthyAt', Types::DATETIME_IMMUTABLE, 'marked_unhealthy_at'); $builder->addNullableField('unHealthySince', Types::DATETIME_IMMUTABLE, 'unhealthy_since'); $builder->addNullableField('lastNotificationSentAt', Types::DATETIME_IMMUTABLE, 'last_notification_sent_at'); } /** * Prepares the metadata for API usage. */ public static function loadApiMetadata(ApiMetadataDriver $metadata): void { $metadata->setGroupPrefix('hook') ->addListProperties( [ 'id', 'name', 'description', 'webhookUrl', 'secret', 'eventsOrderbyDir', 'category', 'triggers', ] ) ->build(); } public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint( 'name', new NotBlank( [ 'message' => 'mautic.core.name.required', ] ) ); $metadata->addPropertyConstraint( 'webhookUrl', new Assert\Url( [ 'message' => 'mautic.core.valid_url_required', ] ) ); $metadata->addPropertyConstraint( 'webhookUrl', new NotBlank( [ 'message' => 'mautic.core.valid_url_required', ] ) ); $metadata->addPropertyConstraint( 'eventsOrderbyDir', new Assert\Choice( [ null, Order::Ascending->value, Order::Descending->value, ] ) ); } /** * @return int */ public function getId() { return $this->id; } /** * @param string $name * * @return Webhook */ public function setName($name) { $this->isChanged('name', $name); $this->name = $name; return $this; } /** * @return string */ public function getName() { return $this->name; } /** * @param string $description * * @return Webhook */ public function setDescription($description) { $this->isChanged('description', $description); $this->description = $description; return $this; } /** * @return string */ public function getDescription() { return $this->description; } /** * @param string $webhookUrl * * @return Webhook */ public function setWebhookUrl($webhookUrl) { $this->isChanged('webhookUrl', $webhookUrl); $this->webhookUrl = $webhookUrl; return $this; } /** * @return string */ public function getWebhookUrl() { return $this->webhookUrl; } /** * @param ?string $secret * * @return Webhook */ public function setSecret($secret) { $this->isChanged('secret', $secret); $this->secret = $secret; return $this; } /** * @return ?string */ public function getSecret() { return $this->secret; } /** * @return Webhook */ public function setCategory(?Category $category = null) { $this->isChanged('category', $category); $this->category = $category; return $this; } /** * @return Category */ public function getCategory() { return $this->category; } /** * @return Collection */ public function getEvents() { return $this->events; } /** * @param Collection $events * * @return $this */ public function setEvents($events) { $this->isChanged('events', $events); $this->events = $events; foreach ($events as $event) { $event->setWebhook($this); } return $this; } /** * This builds a simple array with subscribed events. */ public function buildTriggers(): void { foreach ($this->events as $event) { $this->triggers[] = $event->getEventType(); } } /** * Takes the array of triggers and builds events from them if they don't exist already. */ public function setTriggers(array $triggers): void { foreach ($triggers as $key) { $this->addTrigger($key); } } /** * Takes a trigger (event type) and builds the Event object form it if it doesn't exist already. * * @param string $key */ public function addTrigger($key): bool { if ($this->eventExists($key)) { return false; } $event = new Event(); $event->setEventType($key); $event->setWebhook($this); $this->addEvent($event); return true; } /** * Check if an event exists comared to its type. * * @param string $key */ public function eventExists($key): bool { foreach ($this->events as $event) { if ($event->getEventType() === $key) { return true; } } return false; } /** * @return $this */ public function addEvent(Event $event) { $this->isChanged('events', $event); $this->events[] = $event; return $this; } /** * @return $this */ public function removeEvent(Event $event) { $this->isChanged('events', $event); $this->removedEvents[] = $event; $this->events->removeElement($event); return $this; } /** * @param string $eventsOrderbyDir */ public function setEventsOrderbyDir($eventsOrderbyDir) { $this->isChanged('eventsOrderbyDir', $eventsOrderbyDir); $this->eventsOrderbyDir = $eventsOrderbyDir; return $this; } /** * @return string */ public function getEventsOrderbyDir() { return $this->eventsOrderbyDir; } /** * Get log entities. * * @return ArrayCollection */ public function getLogs() { return $this->logs; } /** * @return Collection */ public function getLimitedLogs(): Collection { $criteria = Criteria::create() ->setMaxResults(self::LOGS_DISPLAY_LIMIT); return $this->logs->matching($criteria); } /** * @param ArrayCollection $logs * * @return $this */ public function addLogs($logs) { $this->logs = $logs; /** @var Log $log */ foreach ($logs as $log) { $log->setWebhook($this); } return $this; } /** * @return $this */ public function addLog(Log $log) { $this->logs[] = $log; return $this; } /** * @return $this */ public function removeLog(Log $log) { $this->logs->removeElement($log); return $this; } /** * @return array */ public function getPayload() { return $this->payload; } /** * @return Webhook */ public function setPayload($payload) { $this->payload = $payload; return $this; } public function wasModifiedRecently(): bool { $dateModified = $this->getDateModified(); if (null === $dateModified) { return false; } $aWhileBack = (new \DateTime())->modify('-2 days'); if ($dateModified < $aWhileBack) { return false; } return true; } /** * @param string $prop */ protected function isChanged($prop, $val) { $getter = 'get'.ucfirst($prop); $current = $this->$getter(); if ('category' == $prop) { $currentId = ($current) ? $current->getId() : ''; $newId = ($val) ? $val->getId() : null; if ($currentId != $newId) { $this->changes[$prop] = [$currentId, $newId]; } } elseif ('events' == $prop) { $this->changes[$prop] = []; } elseif ($current != $val) { $this->changes[$prop] = [$current, $val]; } else { parent::isChanged($prop, $val); } } public function getMarkedUnhealthyAt(): ?\DateTimeImmutable { return $this->markedUnhealthyAt; } public function setMarkedUnhealthyAt(?\DateTimeImmutable $markedUnhealthyAt): Webhook { $this->isChanged('markedUnhealthyAt', $markedUnhealthyAt); $this->markedUnhealthyAt = $markedUnhealthyAt; return $this; } public function getUnHealthySince(): ?\DateTimeImmutable { return $this->unHealthySince; } public function setUnHealthySince(?\DateTimeImmutable $unHealthySince): self { $this->unHealthySince = $unHealthySince; return $this; } public function getLastNotificationSentAt(): ?\DateTimeImmutable { return $this->lastNotificationSentAt; } public function setLastNotificationSentAt(?\DateTimeImmutable $lastNotificationSentAt): self { $this->lastNotificationSentAt = $lastNotificationSentAt; return $this; } /** * Do not update modified_by and date_modified fields if only DNC or manipulator was changed. * Avoid unnecessary update queries. */ public function shouldSkipSettingModifiedProperties(): bool { $changes = $this->changes; unset($changes['markedUnhealthyAt']); unset($changes['unHealthySince']); unset($changes['lastNotificationSentAt']); return 0 === count($changes); } }