Issues (587)

src/InstantAnalytics.php (1 issue)

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\instantanalytics;
12
13
use Craft;
14
use craft\base\Plugin;
15
use craft\commerce\elements\Order;
16
use craft\commerce\events\LineItemEvent;
17
use craft\commerce\Plugin as Commerce;
18
use craft\events\PluginEvent;
19
use craft\events\RegisterUrlRulesEvent;
20
use craft\events\TemplateEvent;
21
use craft\helpers\UrlHelper;
22
use craft\services\Plugins;
23
use craft\web\twig\variables\CraftVariable;
24
use craft\web\UrlManager;
25
use craft\web\View;
26
use nystudio107\instantanalytics\helpers\Field as FieldHelper;
27
use nystudio107\instantanalytics\helpers\IAnalytics;
28
use nystudio107\instantanalytics\models\Settings;
29
use nystudio107\instantanalytics\services\ServicesTrait;
30
use nystudio107\instantanalytics\twigextensions\InstantAnalyticsTwigExtension;
31
use nystudio107\instantanalytics\variables\InstantAnalyticsVariable;
32
use nystudio107\seomatic\Seomatic;
33
use Twig\Error\LoaderError;
34
use yii\base\Event;
35
36
/** @noinspection MissingPropertyAnnotationsInspection */
37
38
/**
39
 * @author    nystudio107
40
 * @package   InstantAnalytics
41
 * @since     1.0.0
42
 */
43
class InstantAnalytics extends Plugin
44
{
45
    // Traits
46
    // =========================================================================
47
48
    use ServicesTrait;
49
50
    // Constants
51
    // =========================================================================
52
53
    const COMMERCE_PLUGIN_HANDLE = 'commerce';
54
    const SEOMATIC_PLUGIN_HANDLE = 'seomatic';
55
56
    // Static Properties
57
    // =========================================================================
58
59
    /**
60
     * @var InstantAnalytics
61
     */
62
    public static $plugin;
63
64
    /**
65
     * @var Settings
66
     */
67
    public static $settings;
68
69
    /**
70
     * @var Commerce|null
71
     */
72
    public static $commercePlugin;
73
74
    /**
75
     * @var Seomatic|null
76
     */
77
    public static $seomaticPlugin;
78
79
    /**
80
     * @var string
81
     */
82
    public static $currentTemplate = '';
83
84
    /**
85
     * @var bool
86
     */
87
    public static $pageViewSent = false;
88
89
    /**
90
     * @var bool
91
     */
92
    public static $craft31 = false;
93
94
    // Public Properties
95
    // =========================================================================
96
97
    /**
98
     * @var string
99
     */
100
    public $schemaVersion = '1.0.0';
101
102
    /**
103
     * @var bool
104
     */
105
    public $hasCpSection = false;
106
107
    /**
108
     * @var bool
109
     */
110
    public $hasCpSettings = true;
111
112
    // Public Methods
113
    // =========================================================================
114
115
    /**
116
     * @inheritdoc
117
     */
118
    public function init()
119
    {
120
        parent::init();
121
        self::$plugin = $this;
122
        self::$settings = $this->getSettings();
123
        self::$craft31 = version_compare(Craft::$app->getVersion(), '3.1', '>=');
0 ignored issues
show
Documentation Bug introduced by
It seems like version_compare(Craft::a...Version(), '3.1', '>=') can also be of type integer. However, the property $craft31 is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
124
125
        // Determine if Craft Commerce is installed & enabled
126
        self::$commercePlugin = Craft::$app->getPlugins()->getPlugin(self::COMMERCE_PLUGIN_HANDLE);
127
        // Determine if SEOmatic is installed & enabled
128
        self::$seomaticPlugin = Craft::$app->getPlugins()->getPlugin(self::SEOMATIC_PLUGIN_HANDLE);
129
        // Add in our Craft components
130
        $this->addComponents();
131
        // Install our global event handlers
132
        $this->installEventListeners();
133
134
        Craft::info(
135
            Craft::t(
136
                'instant-analytics',
137
                '{name} plugin loaded',
138
                ['name' => $this->name]
139
            ),
140
            __METHOD__
141
        );
142
    }
143
144
    /**
145
     * @inheritdoc
146
     */
147
    public function settingsHtml()
148
    {
149
        $commerceFields = [];
150
151
        if (self::$commercePlugin) {
152
            $productTypes = self::$commercePlugin->getProductTypes()->getAllProductTypes();
153
154
            foreach ($productTypes as $productType) {
155
                $productFields = $this->getPullFieldsFromLayoutId($productType->fieldLayoutId);
156
                /** @noinspection SlowArrayOperationsInLoopInspection */
157
                $commerceFields = \array_merge($commerceFields, $productFields);
158
                if ($productType->hasVariants) {
159
                    $variantFields = $this->getPullFieldsFromLayoutId($productType->variantFieldLayoutId);
160
                    /** @noinspection SlowArrayOperationsInLoopInspection */
161
                    $commerceFields = \array_merge($commerceFields, $variantFields);
162
                }
163
            }
164
        }
165
166
        // Rend the settings template
167
        try {
168
            return Craft::$app->getView()->renderTemplate(
169
                'instant-analytics/settings',
170
                [
171
                    'settings' => $this->getSettings(),
172
                    'commerceFields' => $commerceFields,
173
                ]
174
            );
175
        } catch (LoaderError $e) {
176
            Craft::error($e->getMessage(), __METHOD__);
177
        } catch (\Exception $e) {
178
            Craft::error($e->getMessage(), __METHOD__);
179
        }
180
181
        return '';
182
    }
183
184
    /**
185
     * Handle the `{% hook iaSendPageView %}`
186
     *
187
     * @param array &$context
188
     *
189
     * @return string|null
190
     */
191
    public function iaSendPageView(/** @noinspection PhpUnusedParameterInspection */ array &$context)
192
    {
193
        $this->sendPageView();
194
195
        return '';
196
    }
197
198
    // Protected Methods
199
    // =========================================================================
200
201
    /**
202
     * Add in our Craft components
203
     */
204
    protected function addComponents()
205
    {
206
        $view = Craft::$app->getView();
207
        // Add in our Twig extensions
208
        $view->registerTwigExtension(new InstantAnalyticsTwigExtension());
209
        // Install our template hook
210
        $view->hook('iaSendPageView', [$this, 'iaSendPageView']);
211
        // Register our variables
212
        Event::on(
213
            CraftVariable::class,
214
            CraftVariable::EVENT_INIT,
215
            function (Event $event) {
216
                /** @var CraftVariable $variable */
217
                $variable = $event->sender;
218
                $variable->set('instantAnalytics', [
219
                    'class' => InstantAnalyticsVariable::class,
220
                    'viteService' => $this->vite,
221
                ]);
222
            }
223
        );
224
    }
225
226
    /**
227
     * Install our event listeners
228
     */
229
    protected function installEventListeners()
230
    {
231
        // Handler: Plugins::EVENT_AFTER_INSTALL_PLUGIN
232
        Event::on(
233
            Plugins::class,
234
            Plugins::EVENT_AFTER_INSTALL_PLUGIN,
235
            function (PluginEvent $event) {
236
                if ($event->plugin === $this) {
237
                    $request = Craft::$app->getRequest();
238
                    if ($request->isCpRequest) {
239
                        Craft::$app->getResponse()->redirect(UrlHelper::cpUrl('instant-analytics/welcome'))->send();
240
                    }
241
                }
242
            }
243
        );
244
        $request = Craft::$app->getRequest();
245
        // Install only for non-console site requests
246
        if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest()) {
247
            $this->installSiteEventListeners();
248
        }
249
        // Install only for non-console Control Panel requests
250
        if ($request->getIsCpRequest() && !$request->getIsConsoleRequest()) {
251
            $this->installCpEventListeners();
252
        }
253
    }
254
255
    /**
256
     * Install site event listeners for site requests only
257
     */
258
    protected function installSiteEventListeners()
259
    {
260
        // Handler: UrlManager::EVENT_REGISTER_SITE_URL_RULES
261
        Event::on(
262
            UrlManager::class,
263
            UrlManager::EVENT_REGISTER_SITE_URL_RULES,
264
            function (RegisterUrlRulesEvent $event) {
265
                Craft::debug(
266
                    'UrlManager::EVENT_REGISTER_SITE_URL_RULES',
267
                    __METHOD__
268
                );
269
                // Register our Control Panel routes
270
                $event->rules = array_merge(
271
                    $event->rules,
272
                    $this->customFrontendRoutes()
273
                );
274
            }
275
        );
276
        // Remember the name of the currently rendering template
277
        Event::on(
278
            View::class,
279
            View::EVENT_BEFORE_RENDER_PAGE_TEMPLATE,
280
            function (TemplateEvent $event) {
281
                self::$currentTemplate = $event->template;
282
            }
283
        );
284
        // Remember the name of the currently rendering template
285
        Event::on(
286
            View::class,
287
            View::EVENT_AFTER_RENDER_PAGE_TEMPLATE,
288
            function (TemplateEvent $event) {
289
                if (self::$settings->autoSendPageView) {
290
                    $this->sendPageView();
291
                }
292
            }
293
        );
294
        // Commerce-specific hooks
295
        if (self::$commercePlugin) {
296
            Event::on(Order::class, Order::EVENT_AFTER_COMPLETE_ORDER, function (Event $e) {
297
                $order = $e->sender;
298
                if (self::$settings->autoSendPurchaseComplete) {
299
                    $this->commerce->orderComplete($order);
300
                }
301
            });
302
303
            Event::on(Order::class, Order::EVENT_AFTER_ADD_LINE_ITEM, function (LineItemEvent $e) {
304
                $lineItem = $e->lineItem;
305
                if (self::$settings->autoSendAddToCart) {
306
                    $this->commerce->addToCart($lineItem->order, $lineItem);
307
                }
308
            });
309
310
            // Check to make sure Order::EVENT_AFTER_REMOVE_LINE_ITEM is defined
311
            if (defined(Order::class . '::EVENT_AFTER_REMOVE_LINE_ITEM')) {
312
                Event::on(Order::class, Order::EVENT_AFTER_REMOVE_LINE_ITEM, function (LineItemEvent $e) {
313
                    $lineItem = $e->lineItem;
314
                    if (self::$settings->autoSendRemoveFromCart) {
315
                        $this->commerce->removeFromCart($lineItem->order, $lineItem);
316
                    }
317
                });
318
            }
319
        }
320
    }
321
322
    /**
323
     * Install site event listeners for Control Panel requests only
324
     */
325
    protected function installCpEventListeners()
326
    {
327
    }
328
329
    /**
330
     * Return the custom frontend routes
331
     *
332
     * @return array
333
     */
334
    protected function customFrontendRoutes(): array
335
    {
336
        return [
337
            'instantanalytics/pageViewTrack/<filename:[-\w\.*]+>?' =>
338
                'instant-analytics/track/track-page-view-url',
339
            'instantanalytics/eventTrack/<filename:[-\w\.*]+>?' =>
340
                'instant-analytics/track/track-event-url',
341
        ];
342
    }
343
344
    /**
345
     * @inheritdoc
346
     */
347
    protected function createSettingsModel()
348
    {
349
        return new Settings();
350
    }
351
352
    // Private Methods
353
    // =========================================================================
354
355
    /**
356
     * Send a page view with the pre-loaded IAnalytics object
357
     */
358
    private function sendPageView()
359
    {
360
        $request = Craft::$app->getRequest();
361
        if ($request->getIsSiteRequest() && !$request->getIsConsoleRequest() && !self::$pageViewSent) {
362
            self::$pageViewSent = true;
363
            /** @var IAnalytics $analytics */
364
            $analytics = self::$plugin->ia->getGlobals(self::$currentTemplate);
365
            // Bail if we have no analytics object
366
            if ($analytics === null) {
367
                return;
368
            }
369
            // If SEOmatic is installed, set the page title from it
370
            $this->setTitleFromSeomatic($analytics);
371
            // Send the page view
372
            $response = $analytics->sendPageview();
373
            Craft::info(
374
                Craft::t(
375
                    'instant-analytics',
376
                    'pageView sent, response:: {response}',
377
                    [
378
                        'response' => print_r($response, true),
379
                    ]
380
                ),
381
                __METHOD__
382
            );
383
        } else {
384
            Craft::error(
385
                Craft::t(
386
                    'instant-analytics',
387
                    'Analytics not sent because googleAnalyticsTracking is not set'
388
                ),
389
                __METHOD__
390
            );
391
        }
392
    }
393
394
    /**
395
     * If SEOmatic is installed, set the page title from it
396
     *
397
     * @param $analytics
398
     */
399
    private function setTitleFromSeomatic(IAnalytics $analytics)
400
    {
401
        if (self::$seomaticPlugin && Seomatic::$settings->renderEnabled) {
402
            $titleTag = Seomatic::$plugin->title->get('title');
403
            if ($titleTag) {
404
                $titleArray = $titleTag->renderAttributes();
405
                if (!empty($titleArray['title'])) {
406
                    $analytics->setDocumentTitle($titleArray['title']);
407
                }
408
            }
409
        }
410
    }
411
412
    /**
413
     * @param $layoutId
414
     *
415
     * @return array
416
     */
417
    private function getPullFieldsFromLayoutId($layoutId): array
418
    {
419
        $result = ['' => 'none'];
420
        if ($layoutId === null) {
421
            return $result;
422
        }
423
        $fieldLayout = Craft::$app->getFields()->getLayoutById($layoutId);
424
        if ($fieldLayout) {
425
            $result = FieldHelper::fieldsOfTypeFromLayout(FieldHelper::TEXT_FIELD_CLASS_KEY, $fieldLayout, false);
426
        }
427
428
        return $result;
429
    }
430
}
431