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
|
|||
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 |
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 theid
property of an instance of theAccount
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.