diff --git a/phpstan.neon b/phpstan.neon index a2e5224..ced22c0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,6 @@ includes: - vendor/craftcms/phpstan/phpstan.neon parameters: - level: 4 + level: 9 paths: - src diff --git a/src/PhoneHome.php b/src/PhoneHome.php index 3524547..7e423d3 100644 --- a/src/PhoneHome.php +++ b/src/PhoneHome.php @@ -23,6 +23,9 @@ class PhoneHome extends Plugin public string $schemaVersion = '1.0.0'; public bool $hasCpSettings = true; + /** + * @phpstan-ignore-next-line + */ public static function config(): array { return [ diff --git a/src/endpoints/NotionEndpoint.php b/src/endpoints/NotionEndpoint.php index 1c2a18c..e3a9367 100644 --- a/src/endpoints/NotionEndpoint.php +++ b/src/endpoints/NotionEndpoint.php @@ -2,28 +2,79 @@ namespace viget\phonehome\endpoints; +use DateTimeImmutable; +use DateTimeZone; +use Exception; +use Notion\Databases\Database; +use Notion\Databases\Properties\Date as DateDb; +use Notion\Databases\Properties\MultiSelect as MultiSelectDb; +use Notion\Databases\Properties\PropertyInterface; +use Notion\Databases\Properties\RichTextProperty as RichTextDb; +use Notion\Databases\Properties\Select as SelectDb; +use Notion\Databases\Properties\Url as UrlDb; use Notion\Databases\Query; use Notion\Notion; use Notion\Pages\Page; use Notion\Pages\PageParent; use Notion\Pages\Properties\Date; -use Notion\Pages\Properties\RichTextProperty; +use Notion\Pages\Properties\MultiSelect; +use Notion\Pages\Properties\Select; use Notion\Pages\Properties\Title; use Notion\Pages\Properties\Url; -use viget\phonehome\models\SettingsNotion; use viget\phonehome\models\SitePayload; +use viget\phonehome\models\SitePayloadPlugin; class NotionEndpoint implements EndpointInterface { private const PROPERTY_URL = "Url"; private const PROPERTY_ENVIRONMENT = "Environment"; + private const PROPERTY_CRAFT_EDITION = "Craft Edition"; private const PROPERTY_CRAFT_VERSION = "Craft Version"; private const PROPERTY_PHP_VERSION = "PHP Version"; private const PROPERTY_DB_VERSION = "DB Version"; private const PROPERTY_PLUGINS = "Plugins"; + private const PROPERTY_PLUGIN_VERSIONS = "Plugin Versions"; private const PROPERTY_MODULES = "Modules"; private const PROPERTY_DATE_UPDATED = "Date Updated"; - private const PROPERTY_TITLE = "Title"; + private const PROPERTY_NAME = "Name"; + + /** + * @var array + * }> + */ + private const PROPERTY_CONFIG = [ + self::PROPERTY_URL => [ + 'class' => UrlDb::class, + ], + self::PROPERTY_ENVIRONMENT => [ + 'class' => SelectDb::class, + ], + self::PROPERTY_CRAFT_EDITION => [ + 'class' => SelectDb::class, + ], + self::PROPERTY_CRAFT_VERSION => [ + 'class' => SelectDb::class, + ], + self::PROPERTY_PHP_VERSION => [ + 'class' => SelectDb::class, + ], + self::PROPERTY_DB_VERSION => [ + 'class' => SelectDb::class, + ], + self::PROPERTY_PLUGINS => [ + 'class' => MultiSelectDb::class, + ], + self::PROPERTY_PLUGIN_VERSIONS => [ + 'class' => MultiSelectDb::class, + ], + self::PROPERTY_MODULES => [ + 'class' => MultiSelectDb::class, + ], + self::PROPERTY_DATE_UPDATED => [ + 'class' => DateDb::class, + ], + ]; public function __construct( private readonly string $secret, @@ -32,26 +83,35 @@ public function __construct( { } + /** + * @throws Exception + */ public function send(SitePayload $payload): void { $notion = Notion::create($this->secret); $database = $notion->databases()->find($this->databaseId); - // Make sure properties are present on page - // TODO only run if needed - $database = $database - ->addProperty(\Notion\Databases\Properties\Url::create(self::PROPERTY_URL)) - ->addProperty(\Notion\Databases\Properties\RichTextProperty::create(self::PROPERTY_ENVIRONMENT)) - ->addProperty(\Notion\Databases\Properties\RichTextProperty::create(self::PROPERTY_CRAFT_VERSION)) - ->addProperty(\Notion\Databases\Properties\RichTextProperty::create(self::PROPERTY_PHP_VERSION)) - ->addProperty(\Notion\Databases\Properties\RichTextProperty::create(self::PROPERTY_DB_VERSION)) - ->addProperty(\Notion\Databases\Properties\RichTextProperty::create(self::PROPERTY_PLUGINS)) - ->addProperty(\Notion\Databases\Properties\RichTextProperty::create(self::PROPERTY_MODULES)) - ->addProperty(\Notion\Databases\Properties\Date::create(self::PROPERTY_DATE_UPDATED)) - ; - - $notion->databases()->update($database); + // Loop through property config and create properties that don't exist on the DB + $updated = false; + foreach (self::PROPERTY_CONFIG as $propertyName => $config) { + $didUpdate = $this->configureProperty( + $propertyName, + $config['class'], + $database + ); + + // Always stay true if one property updated + if ($didUpdate === true) { + $updated = true; + } + } + // Only update if properties have changed + if ($updated) { + $notion->databases()->update($database); + } + + // Find existing DB record for site $query = Query::create() ->changeFilter( Query\TextFilter::property(self::PROPERTY_URL)->equals($payload->siteUrl), @@ -66,16 +126,20 @@ public function send(SitePayload $payload): void $page = $page ?? Page::create($parent); // Update properties - $page = $page->addProperty(self::PROPERTY_TITLE, Title::fromString($payload->siteName)) + $plugins = $payload->plugins->map(fn(SitePayloadPlugin $plugin) => $plugin->id)->all(); + $pluginVersions = $payload->plugins->map(fn(SitePayloadPlugin $plugin) => $plugin->versionedId)->all(); + + $page = $page->addProperty(self::PROPERTY_NAME, Title::fromString($payload->siteName)) ->addProperty(self::PROPERTY_URL, Url::create($payload->siteUrl)) - ->addProperty(self::PROPERTY_ENVIRONMENT, RichTextProperty::fromString($payload->environment)) - ->addProperty(self::PROPERTY_CRAFT_VERSION, RichTextProperty::fromString($payload->craftVersion)) - ->addProperty(self::PROPERTY_PHP_VERSION, RichTextProperty::fromString($payload->phpVersion)) - ->addProperty(self::PROPERTY_DB_VERSION, RichTextProperty::fromString($payload->dbVersion)) - ->addProperty(self::PROPERTY_PLUGINS, RichTextProperty::fromString($payload->plugins)) - ->addProperty(self::PROPERTY_MODULES, RichTextProperty::fromString($payload->modules)) - ->addProperty(self::PROPERTY_DATE_UPDATED, Date::create(new \DateTimeImmutable('now', new \DateTimeZone('UTC')))) - ; + ->addProperty(self::PROPERTY_ENVIRONMENT, Select::fromName($payload->environment)) + ->addProperty(self::PROPERTY_CRAFT_EDITION, Select::fromName($payload->craftEdition)) + ->addProperty(self::PROPERTY_CRAFT_VERSION, Select::fromName($payload->craftVersion)) + ->addProperty(self::PROPERTY_PHP_VERSION, Select::fromName($payload->phpVersion)) + ->addProperty(self::PROPERTY_DB_VERSION, Select::fromName($payload->dbVersion)) + ->addProperty(self::PROPERTY_PLUGINS, MultiSelect::fromNames(...$plugins)) + ->addProperty(self::PROPERTY_PLUGIN_VERSIONS, MultiSelect::fromNames(...$pluginVersions)) + ->addProperty(self::PROPERTY_MODULES, MultiSelect::fromNames(...$payload->modules->all())) + ->addProperty(self::PROPERTY_DATE_UPDATED, Date::create(new DateTimeImmutable('now', new DateTimeZone('UTC')))); if ($isCreate) { $notion->pages()->create($page); @@ -83,4 +147,35 @@ public function send(SitePayload $payload): void $notion->pages()->update($page); } } + + /** + * @param string $propertyName + * @param class-string $propertyClass + * @param Database $database Pass by reference because there's some immutable stuff going on in the Notion lib + * @return bool True if property was created + * @throws Exception + */ + private function configureProperty(string $propertyName, string $propertyClass, Database &$database): bool + { + $existingProperties = $database->properties()->getAll(); + $existingProperty = $existingProperties[$propertyName] ?? null; + + // Don't configure a property if it already exists and has same type + if ($existingProperty && $existingProperty::class === $propertyClass) { + return false; + } + + // If you're using a class that isn't in this list, most likely the ::create + // method is compatible. But it's worth double-checking. + $database = match ($propertyClass) { + UrlDb::class, + SelectDb::class, + MultiSelectDb::class, + RichTextDb::class, + DateDb::class => $database->addProperty($propertyClass::create($propertyName)), + default => throw new Exception("createProperty doesnt support the class $propertyClass. Double check that its ::create method is compatible and add to this method") + }; + + return true; + } } \ No newline at end of file diff --git a/src/models/SettingsNotion.php b/src/models/SettingsNotion.php deleted file mode 100644 index ecc395c..0000000 --- a/src/models/SettingsNotion.php +++ /dev/null @@ -1,13 +0,0 @@ - $plugins */ + public readonly Collection $plugins, + /** @var Collection $modules */ + public readonly Collection $modules ) { } public static function fromSite(Site $site): self { + $siteUrl = $site->getBaseUrl(); + $environment = Craft::$app->env; + + if (!$siteUrl || !$environment) { + throw new \Exception('$siteUrl or $environment not found'); + } + return new self( - siteUrl: $site->getBaseUrl(), + siteUrl: $siteUrl, siteName: $site->name, - environment: Craft::$app->env, - craftVersion: App::editionName(Craft::$app->getEdition()), + environment: $environment, + craftEdition: App::editionName(Craft::$app->getEdition()), + craftVersion: App::normalizeVersion(Craft::$app->getVersion()), phpVersion: App::phpVersion(), dbVersion: self::_dbDriver(), - plugins: self::_plugins(), + plugins: Collection::make(Craft::$app->plugins->getAllPlugins()) + ->map(SitePayloadPlugin::fromPluginInterface(...)) + ->values(), modules: self::_modules() ); } @@ -56,26 +72,12 @@ private static function _dbDriver(): string return $driverName . ' ' . App::normalizeVersion($db->getSchema()->getServerVersion()); } - /** - * Returns the list of plugins and versions - * - * @return string - */ - private static function _plugins(): string - { - $plugins = Craft::$app->plugins->getAllPlugins(); - - return implode(PHP_EOL, array_map(function($plugin) { - return "{$plugin->name} ({$plugin->developer}): {$plugin->version}"; - }, $plugins)); - } - /** * Returns the list of modules * - * @return string + * @return Collection */ - private static function _modules(): string + private static function _modules(): Collection { $modules = []; @@ -93,7 +95,8 @@ private static function _modules(): string } } - return implode(PHP_EOL, $modules); + // ->values() forces a 0 indexed array + return Collection::make($modules)->values(); } } \ No newline at end of file diff --git a/src/models/SitePayloadPlugin.php b/src/models/SitePayloadPlugin.php new file mode 100644 index 0000000..980afdd --- /dev/null +++ b/src/models/SitePayloadPlugin.php @@ -0,0 +1,26 @@ +id, + versionedId: $pluginInterface->id . ':' . $pluginInterface->version, + ); + } +} \ No newline at end of file diff --git a/src/services/PhoneHomeService.php b/src/services/PhoneHomeService.php index 2e168ec..a063d5f 100644 --- a/src/services/PhoneHomeService.php +++ b/src/services/PhoneHomeService.php @@ -4,6 +4,7 @@ use Craft; use craft\helpers\Queue; +use craft\web\Request; use Illuminate\Support\Collection; use viget\phonehome\endpoints\EndpointInterface; use viget\phonehome\jobs\SendPayloadJob; @@ -29,13 +30,13 @@ public function tryQueuePhoneHome(): void Craft::$app->getIsInstalled() === false || Craft::$app->getRequest()->getIsConsoleRequest() || !Craft::$app->getRequest()->getIsCpRequest() // Only run on CP request - || $request->getIsAjax() + || $request instanceof Request && $request->getIsAjax() ) { return; } // Only run when the cache is empty (once per day at most) - if (Craft::$app->getCache()->get(self::CACHE_KEY) !== false) { + if (Craft::$app->getCache()?->get(self::CACHE_KEY) !== false) { return; }