diff --git a/.env b/.env new file mode 100644 index 0000000..de3574f --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +SEGMENT_PREPROD_KEY=S3Vk6KAg6wilMfSKfr4JKNGgPYD7L97W +SEGMENT_PROD_KEY=Xn1VxRGYASE3pFBODAmSOs3qkXQ6mKW5 diff --git a/README.md b/README.md index b811dd4..4b14c91 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ PrestaShop: `9.0.0` or later. - front office AJAX endpoints for guest initialization and address form refresh, - back office configuration for enabling or disabling one-page checkout, - checkout layout configuration assets and templates shipped by the module, -- checkout runtime flag exposure for the order page. +- checkout runtime flag exposure for the order page, +- optional Segment PHP client bootstrap (`segmentio/analytics-php`) on module configuration pages (see [`docs/SEGMENT.md`](./docs/SEGMENT.md)). ## Code map @@ -41,11 +42,12 @@ Before opening a PR: ## Local development -### PHP autoload +### PHP dependencies and autoload -From the repository root: +From the repository root, install Composer dependencies (required for `segmentio/analytics-php` and the module autoload): ```bash +composer install -d composer dump-autoload -d ``` @@ -101,6 +103,7 @@ For end-to-end checks, use the dedicated runbook: - implementation rules: [`docs/RULES.md`](./docs/RULES.md) - architectural decisions: [`docs/DECISIONS.md`](./docs/DECISIONS.md) +- Segment (configuration & usage): [`docs/SEGMENT.md`](./docs/SEGMENT.md) - contributors: [`CONTRIBUTORS.md`](./CONTRIBUTORS.md) ## License diff --git a/composer.json b/composer.json index 059b3d7..be3cd20 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ } ], "require": { - "php": ">=8.1" + "php": ">=8.1", + "segmentio/analytics-php": "^3.0" }, "require-dev": { "prestashop/php-dev-tools": "^4.3" diff --git a/composer.lock b/composer.lock index 6dfa610..8e3056d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,73 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "91084f1632e99598f94fab64de8a1706", - "packages": [], + "content-hash": "9d5eef215c71c3bf009299c83dbcdabf", + "packages": [ + { + "name": "segmentio/analytics-php", + "version": "3.8.2", + "source": { + "type": "git", + "url": "https://github.com/segmentio/analytics-php.git", + "reference": "6199b9887e53e11dfdfb590beba79577c120efed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/segmentio/analytics-php/zipball/6199b9887e53e11dfdfb590beba79577c120efed", + "reference": "6199b9887e53e11dfdfb590beba79577c120efed", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "overtrue/phplint": "^3.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpunit/phpunit": "~9.5", + "roave/security-advisories": "dev-latest", + "slevomat/coding-standard": "^7.0", + "squizlabs/php_codesniffer": "^3.6" + }, + "suggest": { + "ext-curl": "For using the curl HTTP client", + "ext-zlib": "For using compression" + }, + "bin": [ + "bin/analytics" + ], + "type": "library", + "autoload": { + "psr-4": { + "Segment\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Segment.io ", + "homepage": "https://segment.com/" + } + ], + "description": "Segment Analytics PHP Library", + "homepage": "https://segment.com/libraries/php", + "keywords": [ + "analytics", + "analytics.js", + "segment", + "segmentio" + ], + "support": { + "issues": "https://github.com/segmentio/analytics-php/issues", + "source": "https://github.com/segmentio/analytics-php/tree/3.8.2" + }, + "time": "2026-03-11T18:19:46+00:00" + } + ], "packages-dev": [ { "name": "clue/ndjson-react", @@ -2948,12 +3013,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": ">=8.1" }, - "platform-dev": [], - "plugin-api-version": "2.2.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } diff --git a/docs/SEGMENT.md b/docs/SEGMENT.md new file mode 100644 index 0000000..5898e29 --- /dev/null +++ b/docs/SEGMENT.md @@ -0,0 +1,67 @@ +# Segment dans `ps_onepagecheckout` + +Ce document décrit la **configuration** et l’**intégration technique** de Segment via le **SDK PHP** (`segmentio/analytics-php`), sur le même principe que le module [PrestaShop autoupgrade](https://github.com/PrestaShop/autoupgrade) (classe [`Analytics`](https://github.com/PrestaShop/autoupgrade/blob/dev/classes/Analytics.php)). + +**Comportement actuel** : initialisation de `Segment::init()` lorsque les conditions sont réunies. **Aucun** appel `track` / `flush` n’est encore envoyé depuis le module — cela fera l’objet de tickets ultérieurs. + +**Résumé** : write key lue depuis un fichier/variables d’environnement (`SEGMENT_PREPROD_KEY`, `SEGMENT_PROD_KEY`) → `Segment::init()` sur les requêtes BO qui passent par le hook concerné, **dès que le module est activé**. + +La classe PHP s’appelle **`Analytics`** (nom volontairement **générique**) pour limiter les renommages si le fournisseur change. + +**Front office** : aucun chargement du SDK navigateur (`analytics.js`) ; l’ancien bundle `opc-segment-init` a été retiré. + +## Dépendance Composer + +- `segmentio/analytics-php` (voir `composer.json`). Après clone : `composer install` à la racine du module pour disposer du vendeur et de l’autoload. + +## Clés et constantes + +| Source | Identifiant | Rôle | Défaut | +|--------|-------------|------|--------| +| Environnement | `SEGMENT_PREPROD_KEY` | Write key de la **source PHP** Segment (préprod) — **seule source de vérité** (pas de `configuration`). | `''` | +| Environnement | `SEGMENT_PROD_KEY` | Write key de la **source PHP** Segment (prod) — **seule source de vérité** (pas de `configuration`). | `''` | + +### Règle de sélection de la clé + +- Si `_PS_MODE_DEV_` est `false` → utilisation de `SEGMENT_PROD_KEY` +- Si `_PS_MODE_DEV_` est `true` → utilisation de `SEGMENT_PREPROD_KEY` + +## Architecture PHP + +| Fichier | Rôle | +|---------|------| +| `src/Analytics/Analytics.php` | `bootstrap(bool $moduleSegmentEnabled)` : vérifie activation module, clé non vide, puis `Segment\Segment::init($writeKey)`. Garde statique pour n’initialiser qu’une fois par requête. | + +`ps_onepagecheckout.php` appelle `Analytics::bootstrap(true)` depuis `bootstrapPhpSegmentClient()` (Segment activé tant que le module est activé), invoqué dans `hookActionAdminControllerSetMedia` lorsque `isBackOfficeConfigurationContext()` est vrai. + +### Différences notables avec l’ancienne version (navigateur) + +- Plus de `window.psopc_segment` ni de `opc-segment-init.bundle.js`. +- Les clés **PHP** (`SEGMENT_PREPROD_KEY` / `SEGMENT_PROD_KEY`) ne sont pas la même source Segment que l’ancienne clé **JavaScript** ; à configurer dans l’espace Segment (source PHP). + +## Où cela s’exécute + +Hook **`actionAdminControllerSetMedia`**, uniquement sur la **configuration du module** en BO (`AdminPsOnePageCheckout`, `configure=ps_onepagecheckout`, ou `AdminPsOnePageCheckoutController`), comme auparavant pour le chargement JS — sauf qu’il ne s’agit plus que d’initialiser le client PHP. + +## Configuration Back Office + +Segment est considéré **activé** tant que le module est activé (et si la write key est non vide). + +## Événements (`track`) — à venir + +Les appels `Segment::track()` / `flush()` seront ajoutés dans de futurs tickets, une fois les événements métier définis. + +## Cycle de vie du module + +Pas de clé de configuration dédiée à Segment : l’activation suit l’activation du module. + +## Fichiers utiles + +- `src/Analytics/Analytics.php` +- `ps_onepagecheckout.php` — `hookActionAdminControllerSetMedia`, `bootstrapPhpSegmentClient()` +- `composer.json` — dépendance `segmentio/analytics-php` + +## Limites + +- La write key est fournie par environnement (local / préprod / prod) : à configurer côté plateforme (variable d’environnement / secret). +- `Segment::init` est appelé dans le contexte des requêtes qui déclenchent le hook (typiquement pages de config du module en BO). diff --git a/ps_onepagecheckout.php b/ps_onepagecheckout.php index f471dac..caeda9f 100644 --- a/ps_onepagecheckout.php +++ b/ps_onepagecheckout.php @@ -12,6 +12,7 @@ require __DIR__ . '/vendor/autoload.php'; } +use PrestaShop\Module\PsOnePageCheckout\Analytics\Analytics; use PrestaShop\Module\PsOnePageCheckout\Checkout\OnePageCheckoutAvailability; use PrestaShop\Module\PsOnePageCheckout\Checkout\OnePageCheckoutProcessBuilder; use PrestaShop\Module\PsOnePageCheckout\Form\BackOfficeConfigurationForm; @@ -66,6 +67,7 @@ public function install() && $this->initializeCheckoutProcessProviderConfiguration() && $this->registerHook('actionCheckoutBuildProcess') && $this->registerHook('actionFrontControllerSetMedia') + && $this->registerHook('actionAdminControllerSetMedia') && $this->registerHook('actionFrontControllerSetVariables'); } @@ -155,6 +157,15 @@ public function hookActionFrontControllerSetMedia(): void $this->registerOpcJavascriptAssets(); } + public function hookActionAdminControllerSetMedia(): void + { + if (!isset($this->context->controller) || !$this->isBackOfficeConfigurationContext()) { + return; + } + + $this->bootstrapPhpSegmentClient(); + } + public function hookActionFrontControllerSetVariables(array $params): void { if (!isset($this->context->controller) || $this->context->controller->php_self !== 'order') { @@ -331,4 +342,27 @@ protected function uninstallOnePageCheckoutConfiguration(): bool { return Configuration::deleteByName(self::CONFIG_ONE_PAGE_CHECKOUT_ENABLED); } + + protected function bootstrapPhpSegmentClient(): void + { + // Segment PHP bootstrap is enabled whenever the module is enabled. + // Bootstrap is disabled only when the write key is empty. + Analytics::bootstrap(true); + } + + protected function isBackOfficeConfigurationContext(): bool + { + $controllerName = (string) Tools::getValue('controller'); + if ($controllerName === 'AdminPsOnePageCheckout') { + return true; + } + + $configuredModule = trim((string) Tools::getValue('configure')); + if ($configuredModule === $this->name) { + return true; + } + + return isset($this->context->controller) + && in_array(get_class($this->context->controller), ['AdminPsOnePageCheckoutController'], true); + } } diff --git a/src/Analytics/Analytics.php b/src/Analytics/Analytics.php new file mode 100644 index 0000000..1d90794 --- /dev/null +++ b/src/Analytics/Analytics.php @@ -0,0 +1,76 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\PsOnePageCheckout\Analytics; + +use Segment\Segment; + +/** + * Segment PHP SDK bootstrap, same approach as + * {@link https://github.com/PrestaShop/autoupgrade/blob/dev/classes/Analytics.php PrestaShop\Module\AutoUpgrade\Analytics}. + * + * Event tracking (track) will be added in follow-up work. + */ +final class Analytics +{ + /** + * Segment PHP source write keys env vars — single source of truth (not stored in configuration). + */ + public const SEGMENT_PREPROD_KEY = 'SEGMENT_PREPROD_KEY'; + public const SEGMENT_PROD_KEY = 'SEGMENT_PROD_KEY'; + + private static bool $clientInitialized = false; + + public static function bootstrap(bool $moduleSegmentEnabled): void + { + if (!$moduleSegmentEnabled || self::$clientInitialized) { + return; + } + + $writeKey = self::getWriteKey(); + if ($writeKey === '') { + return; + } + + Segment::init($writeKey); + self::$clientInitialized = true; + } + + private static function getWriteKey(): string + { + $isDevMode = defined('_PS_MODE_DEV_') && (bool) _PS_MODE_DEV_; + $writeKeyEnvVar = $isDevMode ? self::SEGMENT_PREPROD_KEY : self::SEGMENT_PROD_KEY; + + return self::getEnv($writeKeyEnvVar); + } + + private static function getEnv(string $name): string + { + $value = getenv($name); + if ($value === false) { + $value = $_ENV[$name] ?? ''; + } + + return trim((string) $value); + } +} diff --git a/src/Analytics/index.php b/src/Analytics/index.php new file mode 100644 index 0000000..3011d90 --- /dev/null +++ b/src/Analytics/index.php @@ -0,0 +1,29 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/src/Form/BackOfficeConfigurationForm.php b/src/Form/BackOfficeConfigurationForm.php index fc0a48c..d50f628 100644 --- a/src/Form/BackOfficeConfigurationForm.php +++ b/src/Form/BackOfficeConfigurationForm.php @@ -39,8 +39,10 @@ class BackOfficeConfigurationForm private \Module $module; private string $configurationKey; - public function __construct(\Module $module, string $configurationKey) - { + public function __construct( + \Module $module, + string $configurationKey, + ) { $this->module = $module; $this->configurationKey = $configurationKey; } diff --git a/tests/php/Unit/Analytics/AnalyticsTest.php b/tests/php/Unit/Analytics/AnalyticsTest.php new file mode 100644 index 0000000..084d819 --- /dev/null +++ b/tests/php/Unit/Analytics/AnalyticsTest.php @@ -0,0 +1,48 @@ +registerHookCalls); } @@ -222,10 +223,25 @@ public function testHookActionFrontControllerSetMediaAssignsFlagAndSkipsAssetsWh /** @var DummySmarty $smarty */ $smarty = $module->getModuleContext()->smarty; self::assertFalse($smarty->assigned['is_one_page_checkout_enabled']); - self::assertSame([], $module->javascriptDefinitions); + self::assertCount(0, $module->javascriptDefinitions); self::assertSame(0, $module->registeredJavascriptAssetsCalls); } + public function testHookActionAdminControllerSetMediaBootstrapsPhpSegmentOnConfigurationPage(): void + { + $_GET['configure'] = 'ps_onepagecheckout'; + $module = $this->createModule(); + $module->name = 'ps_onepagecheckout'; + $module->setModuleContext((object) [ + 'controller' => (object) ['php_self' => 'adminmodules'], + ]); + + $module->hookActionAdminControllerSetMedia(); + + self::assertSame(1, $module->bootstrapPhpSegmentClientCalls); + self::assertCount(0, $module->javascriptDefinitions); + } + public function testMainModuleFileDoesNotContainCustomAutoloaderRegistration(): void { $mainFile = (string) file_get_contents(_PS_ROOT_DIR_ . '/modules/ps_onepagecheckout/ps_onepagecheckout.php'); @@ -271,6 +287,7 @@ class TestablePsOnepagecheckoutModule extends \Ps_Onepagecheckout public array $javascriptDefinitions = []; public int $registeredJavascriptAssetsCalls = 0; + public int $bootstrapPhpSegmentClientCalls = 0; public bool $isEnabled = true; public int $disableCurrentContextCalls = 0; public bool $disableCurrentContextResult = true; @@ -372,6 +389,12 @@ protected function registerOpcJavascriptAssets(): void ++$this->registeredJavascriptAssetsCalls; } + protected function bootstrapPhpSegmentClient(): void + { + ++$this->bootstrapPhpSegmentClientCalls; + parent::bootstrapPhpSegmentClient(); + } + protected function disableInParent(bool $forceAll): bool { $this->disableInParentCalls[] = $forceAll;