Issues (653)

src/InstantAnalytics.php (3 issues)

1
<?php
2
/**
3
 * Instant Analytics plugin for Craft CMS
4
 *
5
 * Instant Analytics brings full Google Analytics support to your Twig templates
6
 *
7
 * @link      https://nystudio107.com
8
 * @copyright Copyright (c) 2017 nystudio107
9
 */
10
11
namespace nystudio107\instantanalyticsGa4;
12
13
use Craft;
14
use craft\base\Model;
15
use craft\base\Plugin;
16
use craft\commerce\elements\Order;
17
use craft\commerce\events\LineItemEvent;
18
use craft\commerce\Plugin as Commerce;
19
use craft\events\PluginEvent;
20
use craft\events\RegisterUrlRulesEvent;
21
use craft\events\TemplateEvent;
22
use craft\helpers\App;
23
use craft\helpers\UrlHelper;
24
use craft\services\Plugins;
25
use craft\web\twig\variables\CraftVariable;
26
use craft\web\UrlManager;
27
use craft\web\View;
28
use Exception;
29
use nystudio107\instantanalyticsGa4\helpers\Analytics;
30
use nystudio107\instantanalyticsGa4\helpers\Field as FieldHelper;
31
use nystudio107\instantanalyticsGa4\models\Settings;
32
use nystudio107\instantanalyticsGa4\services\ServicesTrait;
33
use nystudio107\instantanalyticsGa4\twigextensions\InstantAnalyticsTwigExtension;
34
use nystudio107\instantanalyticsGa4\variables\InstantAnalyticsVariable;
35
use nystudio107\seomatic\Seomatic;
36
use yii\base\Event;
37
use yii\web\Response;
38
use function array_merge;
39
40
/** @noinspection MissingPropertyAnnotationsInspection */
41
42
/**
43
 * @author    nystudio107
44
 * @package   InstantAnalytics
45
 * @since     1.0.0
46
 */
47
class InstantAnalytics extends Plugin
48
{
49
    // Traits
50
    // =========================================================================
51
52
    use ServicesTrait;
53
54
    // Constants
55
    // =========================================================================
56
57
    /**
58
     * @var string
59
     */
60
    protected const COMMERCE_PLUGIN_HANDLE = 'commerce';
61
62
    /**
63
     * @var string
64
     */
65
    protected const SEOMATIC_PLUGIN_HANDLE = 'seomatic';
66
67
    // Static Properties
68
    // =========================================================================
69
70
    /**
71
     * @var null|InstantAnalytics
72
     */
73
    public static ?InstantAnalytics $plugin = null;
74
75
    /**
76
     * @var null|Settings
77
     */
78
    public static ?Settings $settings = null;
79
80
    /**
81
     * @var null|Commerce
82
     */
83
    public static ?Commerce $commercePlugin = null;
84
85
    /**
86
     * @var null|Seomatic
87
     */
88
    public static ?Seomatic $seomaticPlugin = null;
89
90
    /**
91
     * @var string
92
     */
93
    public static string $currentTemplate = '';
94
95
    /**
96
     * @var bool
97
     */
98
    public static bool $pageViewSent = false;
99
100
    // Public Properties
101
    // =========================================================================
102
103
    /**
104
     * @var string
105
     */
106
    public string $schemaVersion = '1.0.0';
107
108
    /**
109
     * @var bool
110
     */
111
    public bool $hasCpSection = false;
112
113
    /**
114
     * @var bool
115
     */
116
    public bool $hasCpSettings = true;
117
118
    // Public Methods
119
    // =========================================================================
120
121
    /**
122
     * @inheritdoc
123
     */
124
    public function init(): void
125
    {
126
        parent::init();
127
        self::$plugin = $this;
128
        /** @var Settings $settings */
129
        $settings = $this->getSettings();
130
        self::$settings = $settings;
131
132
        // Add in our Craft components
133
        $this->addComponents();
134
        // Install our global event handlers
135
        $this->installEventListeners();
136
137
        Craft::info(
138
            Craft::t(
139
                'instant-analytics-ga4',
140
                '{name} plugin loaded',
141
                ['name' => $this->name]
142
            ),
143
            __METHOD__
144
        );
145
    }
146
147
    /**
148
     * Handle the `{% hook iaSendPageView %}`
149
     */
150
    public function iaSendPageView(/** @noinspection PhpUnusedParameterInspection */ array &$context = []): string
151
    {
152
        $this->ga4->addPageViewEvent();
153
        return '';
154
    }
155
156
    /**
157
     * Handle the `{% hook iaInsertGtag %}`
158
     */
159
    public function iaInsertGtag(/** @noinspection PhpUnusedParameterInspection */ array &$context = []): string
160
    {
161
        $config = [];
162
163
        if (self::$settings->sendUserId) {
164
            $userId = Analytics::getUserId();
165
            if (!empty($userId)) {
166
                $config['user_id'] = $userId;
167
            }
168
        }
169
170
        $measurementId = App::parseEnv(self::$settings->googleAnalyticsMeasurementId);
171
172
173
        $view = Craft::$app->getView();
174
        $existingMode = $view->getTemplateMode();
175
        $view->setTemplateMode(View::TEMPLATE_MODE_CP);
176
        $content = $view->renderTemplate('instant-analytics-ga4/_includes/gtag', compact('measurementId', 'config'));
177
        $view->setTemplateMode($existingMode);
178
179
        return $content;
180
    }
181
182
    public function logAnalyticsEvent(string $message, array $variables = [], string $category = ''): void
183
    {
184
        Craft::info(
185
            Craft::t('instant-analytics-ga4', $message, $variables),
186
            $category
187
        );
188
    }
189
190
    /**
191
     * @inheritdoc
192
     */
193
    protected function settingsHtml(): ?string
194
    {
195
        $commerceFields = [];
196
197
        if (self::$commercePlugin !== null) {
198
            $productTypes = self::$commercePlugin->getProductTypes()->getAllProductTypes();
199
200
            foreach ($productTypes as $productType) {
201
                $productFields = $this->getPullFieldsFromLayoutId($productType->fieldLayoutId);
202
                /** @noinspection SlowArrayOperationsInLoopInspection */
203
                $commerceFields = array_merge($commerceFields, $productFields);
204
                if ($productType->maxVariants > 1) {
205
                    $variantFields = $this->getPullFieldsFromLayoutId($productType->variantFieldLayoutId);
206
                    /** @noinspection SlowArrayOperationsInLoopInspection */
207
                    $commerceFields = array_merge($commerceFields, $variantFields);
208
                }
209
            }
210
        }
211
212
        // Rend the settings template
213
        try {
214
            return Craft::$app->getView()->renderTemplate(
215
                'instant-analytics-ga4/settings',
216
                [
217
                    'settings' => $this->getSettings(),
218
                    'commerceFields' => $commerceFields,
219
                ]
220
            );
221
        } catch (Exception $exception) {
222
            Craft::error($exception->getMessage(), __METHOD__);
223
        }
224
225
        return '';
226
    }
227
    // Protected Methods
228
    // =========================================================================
229
230
    /**
231
     * Add in our Craft components
232
     */
233
    protected function addComponents(): void
234
    {
235
        $view = Craft::$app->getView();
236
        // Add in our Twig extensions
237
        $view->registerTwigExtension(new InstantAnalyticsTwigExtension());
238
        // Install our template hooks
239
        $view->hook('iaSendPageView', [$this, 'iaSendPageView']);
240
        $view->hook('iaInsertGtag', [$this, 'iaInsertGtag']);
241
242
        // Register our variables
243
        Event::on(
244
            CraftVariable::class,
245
            CraftVariable::EVENT_INIT,
246
            function(Event $event): void {
247
                /** @var CraftVariable $variable */
248
                $variable = $event->sender;
249
                $variable->set('instantAnalytics', [
250
                    'class' => InstantAnalyticsVariable::class,
251
                    'viteService' => $this->vite,
252
                ]);
253
            }
254
        );
255
    }
256
257
    /**
258
     * Install our event listeners
259
     */
260
    protected function installEventListeners(): void
261
    {
262
        // Handler: Plugins::EVENT_AFTER_INSTALL_PLUGIN
263
        Event::on(
264
            Plugins::class,
265
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
266
            function(PluginEvent $event): void {
267
                if ($event->plugin === $this) {
268
                    $request = Craft::$app->getRequest();
269
                    if ($request->isCpRequest) {
270
                        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('instant-analytics-ga4/welcome'))->send();
271
                    }
272
                }
273
            }
274
        );
275
276
        // Handler: Plugins::EVENT_AFTER_LOAD_PLUGINS
277
        Event::on(
278
            Plugins::class,
279
            Plugins::EVENT_AFTER_LOAD_PLUGINS,
280
            function() {
281
                // Determine if Craft Commerce is installed & enabled
282
                /** @var Commerce $commercePlugin */
283
                $commercePlugin = Craft::$app->getPlugins()->getPlugin(self::COMMERCE_PLUGIN_HANDLE);
284
                self::$commercePlugin = $commercePlugin;
285
                // Determine if SEOmatic is installed & enabled
286
                /** @var Seomatic $seomaticPlugin */
287
                $seomaticPlugin = Craft::$app->getPlugins()->getPlugin(self::SEOMATIC_PLUGIN_HANDLE);
288
                self::$seomaticPlugin = $seomaticPlugin;
289
290
                // Make sure to install these only after we definitely know whether other plugins are installed
291
                $request = Craft::$app->getRequest();
292
                // Install only for non-console site requests
293
                if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
294
                    $this->installSiteEventListeners();
295
                }
296
297
                // Install only for non-console Control Panel requests
298
                if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
299
                    $this->installCpEventListeners();
300
                }
301
            }
302
        );
303
    }
304
305
    /**
306
     * Install site event listeners for site requests only
307
     */
308
    protected function installSiteEventListeners(): void
309
    {
310
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
311
        Event::on(
312
            UrlManager::class,
313
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
314
            function(RegisterUrlRulesEvent $event): void {
315
                Craft::debug(
316
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
317
                    __METHOD__
318
                );
319
                // Register our Control Panel routes
320
                $event->rules = array_merge(
321
                    $event->rules,
322
                    $this->customFrontendRoutes()
323
                );
324
            }
325
        );
326
        // Remember the name of the currently rendering template
327
        Event::on(
328
            View::class,
329
            View::EVENT_BEFORE_RENDER_PAGE_TEMPLATE,
330
            static function(TemplateEvent $event): void {
331
                self::$currentTemplate = $event->template;
332
            }
333
        );
334
        // Send the page-view event.
335
        Event::on(
336
            View::class,
337
            View::EVENT_AFTER_RENDER_PAGE_TEMPLATE,
338
            function(TemplateEvent $event): void {
0 ignored issues
show
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

338
            function(/** @scrutinizer ignore-unused */ TemplateEvent $event): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
339
                if (self::$settings->autoSendPageView) {
340
                    $request = Craft::$app->getRequest();
341
                    if (!$request->getIsAjax()) {
342
                        $this->ga4->addPageViewEvent();
343
                    }
344
                }
345
            }
346
        );
347
348
        // Send the collected events
349
        Event::on(
350
            Response::class,
351
            Response::EVENT_BEFORE_SEND,
352
            function(Event $event): void {
0 ignored issues
show
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

352
            function(/** @scrutinizer ignore-unused */ Event $event): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
353
                // Initialize this sooner rather than later, since it's possible this will want to tinker with cookies
354
                $this->ga4->getAnalytics();
355
            }
356
        );
357
358
        // Send the collected events
359
        Event::on(
360
            Response::class,
361
            Response::EVENT_AFTER_SEND,
362
            function(Event $event): void {
0 ignored issues
show
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

362
            function(/** @scrutinizer ignore-unused */ Event $event): void {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
363
                $this->ga4->getAnalytics()->sendCollectedEvents();
364
            }
365
        );
366
367
        // Commerce-specific hooks
368
        if (self::$commercePlugin !== null) {
369
            Event::on(Order::class, Order::EVENT_AFTER_COMPLETE_ORDER, function(Event $e): void {
370
                $order = $e->sender;
371
                if (self::$settings->autoSendPurchaseComplete) {
372
                    $this->commerce->triggerOrderCompleteEvent($order);
373
                }
374
            });
375
376
            Event::on(Order::class, Order::EVENT_AFTER_ADD_LINE_ITEM, function(LineItemEvent $e): void {
377
                $lineItem = $e->lineItem;
378
                if (self::$settings->autoSendAddToCart) {
379
                    $this->commerce->triggerAddToCartEvent($lineItem);
380
                }
381
            });
382
383
            // Check to make sure Order::EVENT_AFTER_REMOVE_LINE_ITEM is defined
384
            if (defined(Order::class . '::EVENT_AFTER_REMOVE_LINE_ITEM')) {
385
                Event::on(Order::class, Order::EVENT_AFTER_REMOVE_LINE_ITEM, function(LineItemEvent $e): void {
386
                    $lineItem = $e->lineItem;
387
                    if (self::$settings->autoSendRemoveFromCart) {
388
                        $this->commerce->triggerRemoveFromCartEvent($lineItem);
389
                    }
390
                });
391
            }
392
        }
393
    }
394
395
    /**
396
     * Install site event listeners for Control Panel requests only
397
     */
398
    protected function installCpEventListeners(): void
399
    {
400
    }
401
402
    /**
403
     * Return the custom frontend routes
404
     *
405
     * @return array<string, string>
406
     */
407
    protected function customFrontendRoutes(): array
408
    {
409
        return [
410
            'instantanalytics/pageViewTrack' =>
411
                'instant-analytics-ga4/track/track-page-view-url',
412
            'instantanalytics/eventTrack/<filename:[-\w\.*]+>?' =>
413
                'instant-analytics-ga4/track/track-event-url',
414
        ];
415
    }
416
417
    /**
418
     * @inheritdoc
419
     */
420
    protected function createSettingsModel(): ?Model
421
    {
422
        return new Settings();
423
    }
424
425
    // Private Methods
426
    // =========================================================================
427
428
    /**
429
     * @param $layoutId
430
     *
431
     * @return mixed[]|array<string, string>
432
     */
433
    private function getPullFieldsFromLayoutId($layoutId): array
434
    {
435
        $result = ['' => 'none'];
436
        if ($layoutId === null) {
437
            return $result;
438
        }
439
440
        $fieldLayout = Craft::$app->getFields()->getLayoutById($layoutId);
441
        if ($fieldLayout) {
442
            $result = FieldHelper::fieldsOfTypeFromLayout(FieldHelper::TEXT_FIELD_CLASS_KEY, $fieldLayout, false);
443
        }
444
445
        return $result;
446
    }
447
}
448