diff --git a/Configuration/Page.php b/Configuration/Page.php index 05534cc..81ddf5f 100644 --- a/Configuration/Page.php +++ b/Configuration/Page.php @@ -13,7 +13,6 @@ namespace Farmatholin\SegmentIoBundle\Configuration; -use Doctrine\Common\Annotations\Annotation; use Doctrine\Common\Annotations\Annotation\Required; /** @@ -22,9 +21,9 @@ * @author Vladislav Marin * * @Annotation - * * @Target("METHOD") */ +#[\Attribute(\Attribute::TARGET_METHOD)] class Page implements AnalyticsInterface { @@ -33,20 +32,40 @@ class Page implements AnalyticsInterface * * @var string */ - public $category; + public string $category; /** * @Required * * @var string */ - public $name; + public string $name; /** * @var array */ public array $properties = []; + /** + * @param string $category + */ + public function __construct( + $category = null, + string $name = null, + array $properties = [] + ) { + if (is_array($category)) { + // Doctrine annotation + $this->category = $category['category']; + $this->name = $category['name']; + $this->properties = $category['properties'] ?? $this->properties; + } else { + // PHP Attributes + $this->category = $category; + $this->name = $name; + $this->properties = $properties; + } + } /** * @return string diff --git a/Configuration/Track.php b/Configuration/Track.php index 827d5f2..dd9ca26 100644 --- a/Configuration/Track.php +++ b/Configuration/Track.php @@ -24,6 +24,7 @@ * @Annotation * @Target("METHOD") */ +#[\Attribute(\Attribute::TARGET_METHOD)] class Track implements AnalyticsInterface { /** @@ -31,7 +32,7 @@ class Track implements AnalyticsInterface * * @var string */ - public $event; + public string $event; /** * @var array @@ -48,6 +49,30 @@ class Track implements AnalyticsInterface */ public bool $useTimestamp = false; + /** + * @param string $event + */ + public function __construct( + $event = null, + array $properties = [], + array $context = [], + bool $useTimestamp = false + ) { + if (is_array($event)) { + // Doctrine annotations + $this->event = $event['event']; + $this->properties = $event['properties'] ?? $this->properties; + $this->context = $event['context'] ?? $this->context; + $this->useTimestamp = $event['useTimestamp'] ?? $this->useTimestamp; + } else { + // PHP Attributes + $this->event = $event; + $this->properties = $properties; + $this->context = $context; + $this->useTimestamp = $useTimestamp; + } + } + /** * @return string */ diff --git a/DependencyInjection/SegmentIoExtension.php b/DependencyInjection/SegmentIoExtension.php index 7b080df..fde2dbd 100644 --- a/DependencyInjection/SegmentIoExtension.php +++ b/DependencyInjection/SegmentIoExtension.php @@ -13,8 +13,11 @@ namespace Farmatholin\SegmentIoBundle\DependencyInjection; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\DependencyInjection\Loader; @@ -23,7 +26,7 @@ * * @author Vladislav Marin */ -class SegmentIoExtension extends Extension +class SegmentIoExtension extends Extension implements CompilerPassInterface { /** * @see https://segment.com/docs/connections/data-residency/ @@ -48,6 +51,12 @@ public function load(array $configs, ContainerBuilder $container) $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.xml'); + $loader->load('annotations.xml'); + if (PHP_VERSION_ID >= 80000) { + // PHP Attributes + $loader->load('attributes.xml'); + } + if (isset(static::DATA_RESIDENCIES[$config['data_residency']]) && !isset($config['options']['host'])) { $config['options']['host'] = static::DATA_RESIDENCIES[$config['data_residency']]; } @@ -58,4 +67,16 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('farma.segment_io_env', $config['env']); $container->setParameter('farma.segment_io_options', $config['options']); } + + public function process(ContainerBuilder $container) + { + // Doctrine annotations can be disabled by Symfony Framework or if doctrine/annotations is missing + try { + $container->findDefinition('annotation_reader'); + } catch (ServiceNotFoundException $ignored) { + // Remove Doctrine annotation listener on Kernel request + $container->getDefinition('farma.segment_io.annotation_listener') + ->clearTag('kernel.event_listener'); + } + } } diff --git a/EventListener/AttributeListener.php b/EventListener/AttributeListener.php new file mode 100644 index 0000000..b75f239 --- /dev/null +++ b/EventListener/AttributeListener.php @@ -0,0 +1,96 @@ +segmentIoProvider = $segmentIoProvider; + $this->tokenStorage = $tokenStorage; + $this->guestId = $guestId; + } + + public function onKernelController(ControllerEvent $event): void + { + $controller = $event->getController(); + + if (!is_array($controller)) { + return; + } + + [$controllerObject, $methodName] = $controller; + + $controllerReflectionObject = new \ReflectionObject($controllerObject); + $reflectionMethod = $controllerReflectionObject->getMethod($methodName); + + $userId = $this->getUserId(); + foreach ($reflectionMethod->getAttributes(Page::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + /** @var Page $page */ + $page = $attribute->newInstance(); + + $message = $page->getMessage(); + $message['userId'] = $userId; + + $this->segmentIoProvider->page($message); + + $this->segmentIoProvider->flush(); + } + foreach ($reflectionMethod->getAttributes(Track::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + /** @var Track $track */ + $track = $attribute->newInstance(); + + $message = $track->getMessage(); + $message['userId'] = $userId; + + $this->segmentIoProvider->track($message); + + $this->segmentIoProvider->flush(); + } + } + + /** + * @return string|null + * + * @throws \ReflectionException + */ + private function getUserId(): ?string + { + if (null === $token = $this->tokenStorage->getToken()) { + return $this->guestId; + } + + if (!is_object($user = $token->getUser())) { + return $this->guestId; + } + + $reflect = new \ReflectionClass($user); + if ($reflect->hasMethod('getId')) { + $userId = $user->getId(); + if (null !== $userId) { + return $userId; + } + } + + return $this->guestId; + } +} diff --git a/Resources/config/annotations.xml b/Resources/config/annotations.xml new file mode 100644 index 0000000..0f4f458 --- /dev/null +++ b/Resources/config/annotations.xml @@ -0,0 +1,21 @@ + + + + + + Farmatholin\SegmentIoBundle\EventListener\AnnotationListener + + + + + + + + %farma.segment_io_guest_id% + + + + + diff --git a/Resources/config/attributes.xml b/Resources/config/attributes.xml new file mode 100644 index 0000000..0da5975 --- /dev/null +++ b/Resources/config/attributes.xml @@ -0,0 +1,20 @@ + + + + + + Farmatholin\SegmentIoBundle\EventListener\AttributeListener + + + + + + + %farma.segment_io_guest_id% + + + + + diff --git a/Resources/config/services.xml b/Resources/config/services.xml index df452fc..3d3dd4f 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -30,14 +30,6 @@ - - - - - %farma.segment_io_guest_id% - - - diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index df0df9b..c0229ab 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -11,6 +11,12 @@ Install $ php composer.phar require "farmatholin/segment-io-bundle":"dev-master" +Install Doctrine Annotations package if needed + +.. code-block:: bash + + $ php composer.phar require "doctrine/annotations":"^2.0" + Enable the bundle in the kernel: .. code-block:: php diff --git a/Resources/doc/usage.rst b/Resources/doc/usage.rst index 2598d77..03fc9c4 100644 --- a/Resources/doc/usage.rst +++ b/Resources/doc/usage.rst @@ -39,3 +39,23 @@ If user doesn't exit, id set to 'guest' or from configuration 'guest_id' { // your code } + + +Usage with Attributes +---------------------- + +User for attributes is getting from TokenStorage. +If user doesn't exit, id set to 'guest' or from configuration 'guest_id' + +.. code-block:: php + + use Farmatholin\SegmentIoBundle\Configuration\Page; + use Farmatholin\SegmentIoBundle\Configuration\Track; + + #[Route('/', name: 'homepage')] + #[Page('index', category: 'page', properties: ['foo' => 'bar'])] + #[Track('visit homepage', properties: ['bar' => 'foo'], context: ['aa' => 'bb'], useTimestamp: true)] + public function indexAction(Request $request) + { + // your code + } diff --git a/composer.json b/composer.json index ca81c45..763d469 100644 --- a/composer.json +++ b/composer.json @@ -26,12 +26,14 @@ "symfony/config": ">= 2.7", "symfony/dependency-injection": ">= 2.7", "symfony/http-kernel": ">= 2.7", - "doctrine/annotations": "1.* || 2.*", "symfony/security-core": ">= 2.7" }, "autoload": { "psr-4": { "Farmatholin\\SegmentIoBundle\\": "." } + }, + "suggest": { + "doctrine/annotations": "This package is required to use annotations instead of PHP attributes" } }