Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use select fields where it makes sense #4

Open
wants to merge 9 commits into
base: craft-4
Choose a base branch
from
2 changes: 1 addition & 1 deletion phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ includes:
- vendor/craftcms/phpstan/phpstan.neon

parameters:
level: 4
level: 9
paths:
- src
3 changes: 3 additions & 0 deletions src/PhoneHome.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand Down
147 changes: 121 additions & 26 deletions src/endpoints/NotionEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string,array{
* class: class-string<PropertyInterface>
* }>
*/
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,
],
Comment on lines +65 to +70
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a plugin & plugin versions column.

The plugin column will let you filter by what plugins are installed by site.

But if you ever need to check for specific versions, you can also choose that.

I wish there was a way to do version greater than / less than queries. But I can't think of anything in Notion that would provide that.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How sensitive is this to manipulations to the table? It might be possible to use a formula in a manually-created column but would that confuse the plugin?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Custom columns seem to work fine. As long as their names don't conflict with what's in this list.

self::PROPERTY_MODULES => [
'class' => MultiSelectDb::class,
],
self::PROPERTY_DATE_UPDATED => [
'class' => DateDb::class,
],
];

public function __construct(
private readonly string $secret,
Expand All @@ -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),
Expand All @@ -66,21 +126,56 @@ 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);
} else {
$notion->pages()->update($page);
}
}

/**
* @param string $propertyName
* @param class-string<PropertyInterface> $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;
}
}
13 changes: 0 additions & 13 deletions src/models/SettingsNotion.php

This file was deleted.

49 changes: 26 additions & 23 deletions src/models/SitePayload.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use craft\base\PluginInterface;
use craft\helpers\App;
use craft\models\Site;
use Illuminate\Support\Collection;
use yii\base\Module;

final class SitePayload
Expand All @@ -15,25 +16,40 @@ public function __construct(
public readonly string $siteUrl,
public readonly string $siteName,
public readonly string $environment,
/** @var string $craftEdition - Solo, Team, Pro, etc */
public readonly string $craftEdition,
/** @var string The version number */
public readonly string $craftVersion,
public readonly string $phpVersion,
public readonly string $dbVersion,
public readonly string $plugins,
public readonly string $modules
/** @var Collection<int,SitePayloadPlugin> $plugins */
public readonly Collection $plugins,
/** @var Collection<int,string> $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()
);
}
Expand All @@ -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<int,string>
*/
private static function _modules(): string
private static function _modules(): Collection
{
$modules = [];

Expand All @@ -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();
}

}
26 changes: 26 additions & 0 deletions src/models/SitePayloadPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace viget\phonehome\models;

use craft\base\PluginInterface;

/**
* DTO for transferring plugin info to our Endpoints
*/
class SitePayloadPlugin
{
public function __construct(
public readonly string $id,
public readonly string $versionedId,
)
{
}

public static function fromPluginInterface(PluginInterface $pluginInterface): self
{
return new self(
id: $pluginInterface->id,
versionedId: $pluginInterface->id . ':' . $pluginInterface->version,
);
}
}
5 changes: 3 additions & 2 deletions src/services/PhoneHomeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down