From 59e5b905c5b692e8e22fdb5784354b80a8f54995 Mon Sep 17 00:00:00 2001 From: October CMS Date: Tue, 20 Dec 2022 09:25:24 +1100 Subject: [PATCH 01/74] Set up next branch --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 6cf59f245..4d42b8da9 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^8.0.2", + "php": "^8.1", "composer/composer": "^2.0.0", "doctrine/dbal": "^2.6", "linkorb/jsmin-php": "~1.0", @@ -25,10 +25,10 @@ "league/csv": "~9.1", "nesbot/carbon": "^2.0", "guzzlehttp/guzzle": "^7.2", - "laravel/tinker": "~2.0" + "laravel/tinker": "dev-develop" }, "require-dev": { - "laravel/framework": "^9.0", + "laravel/framework": "dev-master", "phpunit/phpunit": "^8.0|^9.0", "meyfa/phpunit-assert-gd": "^2.0.0|^3.0.0", "phpbench/phpbench": "^1.2" From d2a99aa8636261c3327f72446981f9c57390bf19 Mon Sep 17 00:00:00 2001 From: October CMS Date: Sat, 25 Mar 2023 15:37:38 +1100 Subject: [PATCH 02/74] Bump versions --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 4d42b8da9..3f47ecc8d 100644 --- a/composer.json +++ b/composer.json @@ -25,10 +25,10 @@ "league/csv": "~9.1", "nesbot/carbon": "^2.0", "guzzlehttp/guzzle": "^7.2", - "laravel/tinker": "dev-develop" + "laravel/tinker": "~2.0" }, "require-dev": { - "laravel/framework": "dev-master", + "laravel/framework": "^10.0", "phpunit/phpunit": "^8.0|^9.0", "meyfa/phpunit-assert-gd": "^2.0.0|^3.0.0", "phpbench/phpbench": "^1.2" From 6d15c08a1ee7350c914dff2d36333582a6aa4f60 Mon Sep 17 00:00:00 2001 From: October CMS Date: Wed, 12 Jul 2023 15:53:24 +1000 Subject: [PATCH 03/74] Lower php --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 8987d064d..518b862e2 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ } ], "require": { - "php": "^8.1", + "php": "^8.0.2", "composer/composer": "^2.0.0", "doctrine/dbal": "^2.13.3|^3.1.4", "linkorb/jsmin-php": "~1.0", From 2bf101e18347e8cdfec3586bfc9cd5d1e866cf5e Mon Sep 17 00:00:00 2001 From: October CMS Date: Wed, 12 Jul 2023 15:55:27 +1000 Subject: [PATCH 04/74] Adds auth facade --- helpers/Auth.php | 8 ++++++++ src/Support/Facades/Auth.php | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 helpers/Auth.php create mode 100644 src/Support/Facades/Auth.php diff --git a/helpers/Auth.php b/helpers/Auth.php new file mode 100644 index 000000000..13dfe11ce --- /dev/null +++ b/helpers/Auth.php @@ -0,0 +1,8 @@ + Date: Mon, 17 Jul 2023 12:12:30 +1000 Subject: [PATCH 05/74] Adds agent detection --- composer.json | 1 + src/Foundation/Application.php | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 518b862e2..07d87dc9b 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "php": "^8.0.2", "composer/composer": "^2.0.0", "doctrine/dbal": "^2.13.3|^3.1.4", + "jenssegers/agent": "^2.6", "linkorb/jsmin-php": "~1.0", "wikimedia/less.php": "~3.0", "scssphp/scssphp": "~1.0", diff --git a/src/Foundation/Application.php b/src/Foundation/Application.php index 552229e29..759ed00fd 100644 --- a/src/Foundation/Application.php +++ b/src/Foundation/Application.php @@ -316,30 +316,37 @@ public function registerCoreContainerAliases() { $aliases = [ 'app' => [\October\Rain\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class], + 'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class], + 'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class], 'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class], 'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class], 'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class], + 'cache.psr6' => [\Symfony\Component\Cache\Adapter\Psr16Adapter::class, \Symfony\Component\Cache\Adapter\AdapterInterface::class, \Psr\Cache\CacheItemPoolInterface::class], 'config' => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class], 'cookie' => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class], - 'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class], 'db' => [\October\Rain\Database\DatabaseManager::class], 'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class], 'db.schema' => [\Illuminate\Database\Schema\Builder::class], + 'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class], 'events' => [\October\Rain\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class], 'files' => [\Illuminate\Filesystem\Filesystem::class], 'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class], 'filesystem.disk' => [\Illuminate\Contracts\Filesystem\Filesystem::class], 'filesystem.cloud' => [\Illuminate\Contracts\Filesystem\Cloud::class], 'hash' => [\Illuminate\Contracts\Hashing\Hasher::class], + 'hash.driver' => [\Illuminate\Contracts\Hashing\Hasher::class], 'translator' => [\Illuminate\Translation\Translator::class, \Illuminate\Contracts\Translation\Translator::class], 'log' => [\Illuminate\Log\Logger::class, \Psr\Log\LoggerInterface::class], 'mail.manager' => [\Illuminate\Mail\MailManager::class, \Illuminate\Contracts\Mail\Factory::class], 'mailer' => [\Illuminate\Mail\Mailer::class, \Illuminate\Contracts\Mail\Mailer::class, \Illuminate\Contracts\Mail\MailQueue::class], + 'auth.password' => [\Illuminate\Auth\Passwords\PasswordBrokerManager::class, \Illuminate\Contracts\Auth\PasswordBrokerFactory::class], + 'auth.password.broker' => [\Illuminate\Auth\Passwords\PasswordBroker::class, \Illuminate\Contracts\Auth\PasswordBroker::class], 'queue' => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class], 'queue.connection' => [\Illuminate\Contracts\Queue\Queue::class], 'queue.failer' => [\Illuminate\Queue\Failed\FailedJobProviderInterface::class], 'redirect' => [\Illuminate\Routing\Redirector::class], 'redis' => [\Illuminate\Redis\RedisManager::class, \Illuminate\Contracts\Redis\Factory::class], + 'redis.connection' => [\Illuminate\Redis\Connections\Connection::class, \Illuminate\Contracts\Redis\Connection::class], 'request' => [\Illuminate\Http\Request::class, \Symfony\Component\HttpFoundation\Request::class], 'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class], 'session' => [\Illuminate\Session\SessionManager::class], From 075798fa0f109c44922d5553d83b9e9a7790add1 Mon Sep 17 00:00:00 2001 From: October CMS Date: Tue, 18 Jul 2023 16:35:17 +1000 Subject: [PATCH 06/74] Registers auth provider --- src/Auth/AuthServiceProvider.php | 13 +++++++++++++ .../Providers/AppDeferSupportServiceProvider.php | 1 + .../Providers/AppSupportServiceProvider.php | 1 + 3 files changed, 15 insertions(+) create mode 100644 src/Auth/AuthServiceProvider.php diff --git a/src/Auth/AuthServiceProvider.php b/src/Auth/AuthServiceProvider.php new file mode 100644 index 000000000..23f6c1710 --- /dev/null +++ b/src/Auth/AuthServiceProvider.php @@ -0,0 +1,13 @@ + Date: Wed, 19 Jul 2023 17:27:25 +1000 Subject: [PATCH 07/74] Require 2FA lib --- composer.json | 1 + src/Auth/AuthServiceProvider.php | 13 ------------- src/Foundation/Application.php | 2 -- .../Providers/AppSupportServiceProvider.php | 1 - 4 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 src/Auth/AuthServiceProvider.php diff --git a/composer.json b/composer.json index 07d87dc9b..91537af20 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "jenssegers/agent": "^2.6", "linkorb/jsmin-php": "~1.0", "wikimedia/less.php": "~3.0", + "pragmarx/google2fa": "^8.0", "scssphp/scssphp": "~1.0", "symfony/yaml": "^6.0", "twig/twig": "~3.0", diff --git a/src/Auth/AuthServiceProvider.php b/src/Auth/AuthServiceProvider.php deleted file mode 100644 index 23f6c1710..000000000 --- a/src/Auth/AuthServiceProvider.php +++ /dev/null @@ -1,13 +0,0 @@ - [\October\Rain\Foundation\Application::class, \Illuminate\Contracts\Container\Container::class, \Illuminate\Contracts\Foundation\Application::class], - 'auth' => [\Illuminate\Auth\AuthManager::class, \Illuminate\Contracts\Auth\Factory::class], - 'auth.driver' => [\Illuminate\Contracts\Auth\Guard::class], 'blade.compiler' => [\Illuminate\View\Compilers\BladeCompiler::class], 'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class], 'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class], diff --git a/src/Foundation/Providers/AppSupportServiceProvider.php b/src/Foundation/Providers/AppSupportServiceProvider.php index 863a72215..0e78780bf 100644 --- a/src/Foundation/Providers/AppSupportServiceProvider.php +++ b/src/Foundation/Providers/AppSupportServiceProvider.php @@ -11,7 +11,6 @@ class AppSupportServiceProvider extends AggregateServiceProvider * provides gets the services provided by the provider */ protected $providers = [ - \October\Rain\Auth\AuthServiceProvider::class, \October\Rain\Database\DatabaseServiceProvider::class, \October\Rain\Halcyon\HalcyonServiceProvider::class, \October\Rain\Filesystem\FilesystemServiceProvider::class, From 7d71fa374c5e2ae7dd5e541384845f0629f8bb3b Mon Sep 17 00:00:00 2001 From: October CMS Date: Wed, 19 Jul 2023 18:15:18 +1000 Subject: [PATCH 08/74] Adds qr code lib --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 91537af20..957be8981 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "linkorb/jsmin-php": "~1.0", "wikimedia/less.php": "~3.0", "pragmarx/google2fa": "^8.0", + "bacon/bacon-qr-code": "^2.0", "scssphp/scssphp": "~1.0", "symfony/yaml": "^6.0", "twig/twig": "~3.0", From 0bd5aa38f840760fdcba701d2135471c7d3f39c3 Mon Sep 17 00:00:00 2001 From: October CMS Date: Thu, 20 Jul 2023 16:53:56 +1000 Subject: [PATCH 09/74] Prefer a slash instead of empty string --- src/Html/UrlServiceProvider.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Html/UrlServiceProvider.php b/src/Html/UrlServiceProvider.php index 0b8f45dfe..2e89bb29d 100644 --- a/src/Html/UrlServiceProvider.php +++ b/src/Html/UrlServiceProvider.php @@ -62,7 +62,8 @@ public function registerRelativeHelper() $fullUrl = $provider->to($url); return parse_url($fullUrl, PHP_URL_PATH) . (($query = parse_url($fullUrl, PHP_URL_QUERY)) ? '?' . $query : '') - . (($fragment = parse_url($fullUrl, PHP_URL_FRAGMENT)) ? '#' . $fragment : ''); + . (($fragment = parse_url($fullUrl, PHP_URL_FRAGMENT)) ? '#' . $fragment : '') + ?: '/'; }); } From f115c9fed0d6dcba38e6784a5e85cf5b9812da7f Mon Sep 17 00:00:00 2001 From: October CMS Date: Fri, 21 Jul 2023 20:54:41 +1000 Subject: [PATCH 10/74] Adds Url::toSigned --- src/Html/UrlMixin.php | 72 +++++++++++++++++++++++++++++++++ src/Html/UrlServiceProvider.php | 12 +++--- 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 src/Html/UrlMixin.php diff --git a/src/Html/UrlMixin.php b/src/Html/UrlMixin.php new file mode 100644 index 000000000..fbb7de5f4 --- /dev/null +++ b/src/Html/UrlMixin.php @@ -0,0 +1,72 @@ +provider = $provider; + } + + /** + * toRelative converts a full URL to a relative URL + */ + public function toRelative($url) + { + $fullUrl = $this->provider->to($url); + return parse_url($fullUrl, PHP_URL_PATH) + . (($query = parse_url($fullUrl, PHP_URL_QUERY)) ? '?' . $query : '') + . (($fragment = parse_url($fullUrl, PHP_URL_FRAGMENT)) ? '#' . $fragment : '') + ?: '/'; + } + + /** + * toSigned signs a bare URL that can be validated with hasValidSignature + */ + public function toSigned($url, $expiration = null, $absolute = true) + { + if (!$absolute) { + $url = $this->toRelative($url); + } + + $parameters = []; + + $parts = parse_url($url); + + parse_str($parts['query'] ?? '', $parameters); + + unset($parameters['signature']); + + ksort($parameters); + + if ($expiration) { + unset($parameters['expires']); + $parameters = $parameters + ['expires' => $this->availableAt($expiration)]; + } + + $key = Config::get('app.key'); + + $signUrl = http_build_url($url, http_build_query(['query' => $parameters])); + + $signature = hash_hmac('sha256', $signUrl, $key); + + return http_build_url($url, ['query' => http_build_query($parameters + ['signature' => $signature])]); + } +} diff --git a/src/Html/UrlServiceProvider.php b/src/Html/UrlServiceProvider.php index 2e89bb29d..fd1c4f4ec 100644 --- a/src/Html/UrlServiceProvider.php +++ b/src/Html/UrlServiceProvider.php @@ -58,12 +58,12 @@ public function registerRelativeHelper() { $provider = $this->app['url']; - $provider->macro('toRelative', function($url) use ($provider) { - $fullUrl = $provider->to($url); - return parse_url($fullUrl, PHP_URL_PATH) - . (($query = parse_url($fullUrl, PHP_URL_QUERY)) ? '?' . $query : '') - . (($fragment = parse_url($fullUrl, PHP_URL_FRAGMENT)) ? '#' . $fragment : '') - ?: '/'; + $provider->macro('toRelative', function(...$args) use ($provider) { + return (new \October\Rain\Html\UrlMixin($provider))->toRelative(...$args); + }); + + $provider->macro('toSigned', function(...$args) use ($provider) { + return (new \October\Rain\Html\UrlMixin($provider))->toSigned(...$args); }); } From 6b06350169397beab211790806ed19b8bdefb87c Mon Sep 17 00:00:00 2001 From: October CMS Date: Sun, 23 Jul 2023 17:31:42 +1000 Subject: [PATCH 11/74] Logic error --- src/Html/UrlMixin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Html/UrlMixin.php b/src/Html/UrlMixin.php index fbb7de5f4..73da5a307 100644 --- a/src/Html/UrlMixin.php +++ b/src/Html/UrlMixin.php @@ -63,7 +63,7 @@ public function toSigned($url, $expiration = null, $absolute = true) $key = Config::get('app.key'); - $signUrl = http_build_url($url, http_build_query(['query' => $parameters])); + $signUrl = http_build_url($url, ['query' => http_build_query($parameters)]); $signature = hash_hmac('sha256', $signUrl, $key); From 61c6cdca16b294e9015352e333b3148ddcd3e932 Mon Sep 17 00:00:00 2001 From: October CMS Date: Fri, 15 Sep 2023 14:03:40 +1000 Subject: [PATCH 12/74] Refactor --- src/Halcyon/Model.php | 145 ++++++++++++++++++++++++++++++++---------- 1 file changed, 111 insertions(+), 34 deletions(-) diff --git a/src/Halcyon/Model.php b/src/Halcyon/Model.php index 9175036ed..6d4d64168 100644 --- a/src/Halcyon/Model.php +++ b/src/Halcyon/Model.php @@ -129,6 +129,11 @@ class Model extends Extendable implements ArrayAccess, Arrayable, Jsonable, Json */ protected static $booted = []; + /** + * @var array traitInitializers that will be called on each new instance. + */ + protected static $traitInitializers = []; + /** * __construct a new Halcyon model instance. * @param array $attributes @@ -138,6 +143,8 @@ public function __construct(array $attributes = []) { $this->bootIfNotBooted(); + $this->initializeTraits(); + $this->bootNicerEvents(); parent::__construct(); @@ -152,19 +159,27 @@ public function __construct(array $attributes = []) */ protected function bootIfNotBooted() { - $class = get_class($this); - - if (!isset(static::$booted[$class])) { - static::$booted[$class] = true; + if (!isset(static::$booted[static::class])) { + static::$booted[static::class] = true; $this->fireModelEvent('booting', false); + static::booting(); static::boot(); + static::booted(); $this->fireModelEvent('booted', false); } } + /** + * booting performs any actions required before the model boots. + */ + protected static function booting() + { + // + } + /** * boot is the "booting" method of the model. */ @@ -178,13 +193,49 @@ protected static function boot() */ protected static function bootTraits() { - foreach (class_uses_recursive(get_called_class()) as $trait) { - if (method_exists(get_called_class(), $method = 'boot'.class_basename($trait))) { - forward_static_call([get_called_class(), $method]); + $class = static::class; + + $booted = []; + + static::$traitInitializers[$class] = []; + + foreach (class_uses_recursive($class) as $trait) { + $method = 'boot'.class_basename($trait); + + if (method_exists($class, $method) && ! in_array($method, $booted)) { + forward_static_call([$class, $method]); + + $booted[] = $method; + } + + if (method_exists($class, $method = 'initialize'.class_basename($trait))) { + static::$traitInitializers[$class][] = $method; + + static::$traitInitializers[$class] = array_unique( + static::$traitInitializers[$class] + ); } } } + /** + * initializeTraits on the model. + */ + protected function initializeTraits() + { + foreach (static::$traitInitializers[static::class] as $method) { + $this->{$method}(); + } + } + + /** + * booted performs any actions required after the model boots. + */ + protected static function booted() + { + // + } + /** * clearBootedModels clears the list of booted models so they will be re-booted. */ @@ -1341,7 +1392,7 @@ public function getFileNameParts($fileName = null) } /** - * Get the datasource for the model. + * getDatasource for the model. * * @return \October\Rain\Halcyon\Datasource\DatasourceInterface */ @@ -1351,7 +1402,7 @@ public function getDatasource() } /** - * Get the current datasource name for the model. + * getDatasourceName for the model. * * @return string */ @@ -1361,7 +1412,7 @@ public function getDatasourceName() } /** - * Set the datasource associated with the model. + * setDatasource associated with the model. * * @param string $name * @return $this @@ -1374,7 +1425,7 @@ public function setDatasource($name) } /** - * Resolve a datasource instance. + * resolveDatasource instance. * * @param string|null $datasource * @return \October\Rain\Halcyon\Datasource @@ -1385,7 +1436,7 @@ public static function resolveDatasource($datasource = null) } /** - * Get the datasource resolver instance. + * getDatasourceResolver instance. * * @return \October\Rain\Halcyon\DatasourceResolverInterface */ @@ -1395,7 +1446,7 @@ public static function getDatasourceResolver() } /** - * Set the datasource resolver instance. + * setDatasourceResolver instance. * * @param \October\Rain\Halcyon\Datasource\ResolverInterface $resolver * @return void @@ -1406,7 +1457,7 @@ public static function setDatasourceResolver(Resolver $resolver) } /** - * Unset the datasource resolver for models. + * unsetDatasourceResolver for models. * * @return void */ @@ -1416,7 +1467,7 @@ public static function unsetDatasourceResolver() } /** - * Get the event dispatcher instance. + * getEventDispatcher instance. * * @return \Illuminate\Contracts\Events\Dispatcher */ @@ -1426,7 +1477,7 @@ public static function getEventDispatcher() } /** - * Set the event dispatcher instance. + * setEventDispatcher instance. * * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher * @return void @@ -1437,7 +1488,7 @@ public static function setEventDispatcher(Dispatcher $dispatcher) } /** - * Unset the event dispatcher for models. + * unsetEventDispatcher for models. * * @return void */ @@ -1447,7 +1498,7 @@ public static function unsetEventDispatcher() } /** - * Get the cache manager instance. + * getCacheManager instance. * * @return \Illuminate\Cache\CacheManager */ @@ -1457,7 +1508,7 @@ public static function getCacheManager() } /** - * Set the cache manager instance. + * setCacheManager instance. * * @param \Illuminate\Cache\CacheManager $cache * @return void @@ -1468,7 +1519,7 @@ public static function setCacheManager($cache) } /** - * Unset the cache manager for models. + * unsetCacheManager for models. * * @return void */ @@ -1478,7 +1529,7 @@ public static function unsetCacheManager() } /** - * Initializes the object properties from the cached data. The extra data + * initCacheItem initializes the object properties from the cached data. The extra data * set here becomes available as attributes set on the model after fetch. * @param array $cached The cached data array. */ @@ -1487,7 +1538,7 @@ public static function initCacheItem(&$item) } /** - * Get the mutated attributes for a given instance. + * getMutatedAttributes gets the mutated attributes for a given instance. * * @return array */ @@ -1503,7 +1554,7 @@ public function getMutatedAttributes() } /** - * Extract and cache all the mutated attributes of a class. + * cacheMutatedAttributes extracts and cache all the mutated attributes of a class. * * @param string $class * @return void @@ -1525,7 +1576,7 @@ public static function cacheMutatedAttributes($class) } /** - * Dynamically retrieve attributes on the model. + * __get dynamically retrieve attributes on the model. * * @param string $key * @return mixed @@ -1536,7 +1587,7 @@ public function __get($key) } /** - * Dynamically set attributes on the model. + * __set dynamically set attributes on the model. * * @param string $key * @param mixed $value @@ -1553,7 +1604,7 @@ public function __set($key, $value) } /** - * Determine if the given attribute exists. + * offsetExists determines if the given attribute exists. * * @param mixed $offset * @return bool @@ -1564,7 +1615,7 @@ public function offsetExists($offset): bool } /** - * Get the value for a given offset. + * offsetGet the value for a given offset. * * @param mixed $offset * @return mixed @@ -1575,7 +1626,7 @@ public function offsetGet($offset): mixed } /** - * Set the value for a given offset. + * offsetSet the value for a given offset. * * @param mixed $offset * @param mixed $value @@ -1587,7 +1638,7 @@ public function offsetSet($offset, $value): void } /** - * Unset the value for a given offset. + * offsetUnset the value for a given offset. * * @param mixed $offset * @return void @@ -1598,7 +1649,7 @@ public function offsetUnset($offset): void } /** - * Determine if an attribute exists on the model. + * __isset determines if an attribute exists on the model. * * @param string $key * @return bool @@ -1613,7 +1664,7 @@ public function __isset($key) } /** - * Unset an attribute on the model. + * __unset an attribute on the model. * * @param string $key * @return void @@ -1624,7 +1675,7 @@ public function __unset($key) } /** - * Handle dynamic method calls into the model. + * __call handles dynamic method calls into the model. * * @param string $method * @param array $parameters @@ -1642,7 +1693,7 @@ public function __call($method, $parameters) } /** - * Handle dynamic static method calls into the method. + * __callStatic handles dynamic static method calls into the method. * * @param string $method * @param array $parameters @@ -1656,7 +1707,7 @@ public static function __callStatic($method, $parameters) } /** - * Convert the model to its string representation. + * __toString converts the model to its string representation. * * @return string */ @@ -1664,4 +1715,30 @@ public function __toString() { return $this->toJson(); } + + /** + * __sleep prepare the object for serialization. + */ + public function __sleep() + { + $this->unbindEvent(); + + $this->extendableDestruct(); + + return parent::__sleep(); + } + + /** + * __wakeup when a model is being unserialized, check if it needs to be booted. + */ + public function __wakeup() + { + parent::__wakeup(); + + $this->bootIfNotBooted(); + + $this->initializeTraits(); + + $this->bootNicerEvents(); + } } From a6d15ab3bbf1850fac760d2bc61736314ea20230 Mon Sep 17 00:00:00 2001 From: October CMS Date: Fri, 15 Sep 2023 14:14:20 +1000 Subject: [PATCH 13/74] Refactor events to concern trait --- src/Halcyon/Concerns/HasEvents.php | 562 +++++++++++++++++++++++++++++ src/Halcyon/Model.php | 330 +---------------- 2 files changed, 568 insertions(+), 324 deletions(-) create mode 100644 src/Halcyon/Concerns/HasEvents.php diff --git a/src/Halcyon/Concerns/HasEvents.php b/src/Halcyon/Concerns/HasEvents.php new file mode 100644 index 000000000..1732d3fd7 --- /dev/null +++ b/src/Halcyon/Concerns/HasEvents.php @@ -0,0 +1,562 @@ + 'beforeCreate', + 'created' => 'afterCreate', + 'saving' => 'beforeSave', + 'saved' => 'afterSave', + 'updating' => 'beforeUpdate', + 'updated' => 'afterUpdate', + 'deleting' => 'beforeDelete', + 'deleted' => 'afterDelete', + 'fetching' => 'beforeFetch', + 'fetched' => 'afterFetch', + ]; + + foreach ($nicerEvents as $eventMethod => $method) { + self::$eventMethod(function ($model) use ($method) { + $model->fireEvent('model.' . $method); + + if ($model->methodExists($method)) { + return $model->$method(); + } + }); + } + + // Boot event + $this->fireEvent('model.afterBoot'); + $this->afterBoot(); + + static::$eventsBooted[$class] = true; + } + + /** + * initializeModelEvent is called every time the model is constructed. + */ + protected function initializeModelEvent() + { + $this->fireEvent('model.afterInit'); + $this->afterInit(); + } + + /** + * flushEventListeners removes all of the event listeners for the model. + */ + public static function flushEventListeners() + { + if (!isset(static::$dispatcher)) { + return; + } + + $instance = new static; + + foreach ($instance->getObservableEvents() as $event) { + static::$dispatcher->forget("halcyon.{$event}: ".get_called_class()); + } + + static::$eventsBooted = []; + } + + /** + * getObservableEvents names. + * @return array + */ + public function getObservableEvents() + { + return array_merge( + [ + 'creating', 'created', 'updating', 'updated', + 'deleting', 'deleted', 'saving', 'saved', + 'fetching', 'fetched' + ], + $this->observables + ); + } + + + /** + * setObservableEvents names. + * @param array $observables + * @return $this + */ + public function setObservableEvents(array $observables) + { + $this->observables = $observables; + + return $this; + } + + /** + * addObservableEvents name. + * @param array|mixed $observables + * @return void + */ + public function addObservableEvents($observables) + { + $observables = is_array($observables) ? $observables : func_get_args(); + + $this->observables = array_unique(array_merge($this->observables, $observables)); + } + + /** + * removeObservableEvents name. + * @param array|mixed $observables + * @return void + */ + public function removeObservableEvents($observables) + { + $observables = is_array($observables) ? $observables : func_get_args(); + + $this->observables = array_diff($this->observables, $observables); + } + + /** + * getEventDispatcher instance. + * @return \Illuminate\Contracts\Events\Dispatcher + */ + public static function getEventDispatcher() + { + return static::$dispatcher; + } + + /** + * setEventDispatcher instance. + * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher + * @return void + */ + public static function setEventDispatcher(Dispatcher $dispatcher) + { + static::$dispatcher = $dispatcher; + } + + /** + * unsetEventDispatcher for models. + * @return void + */ + public static function unsetEventDispatcher() + { + static::$dispatcher = null; + } + + /** + * registerModelEvent with the dispatcher. + * @param string $event + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + protected static function registerModelEvent($event, $callback, $priority = 0) + { + if (isset(static::$dispatcher)) { + $name = static::class; + + static::$dispatcher->listen("halcyon.{$event}: {$name}", $callback, $priority); + } + } + + /** + * fireModelEvent for the model. + * @param string $event + * @param bool $halt + * @return mixed + */ + protected function fireModelEvent($event, $halt = true) + { + if (!isset(static::$dispatcher)) { + return true; + } + + // We will append the names of the class to the event to distinguish it from + // other model events that are fired, allowing us to listen on each model + // event set individually instead of catching event for all the models. + $event = "halcyon.{$event}: ".static::class; + + $method = $halt ? 'until' : 'dispatch'; + + return static::$dispatcher->$method($event, $this); + } + + /** + * Create a new native event for handling beforeFetch(). + * @param Closure|string $callback + * @return void + */ + public static function fetching($callback) + { + static::registerModelEvent('fetching', $callback); + } + + /** + * Create a new native event for handling afterFetch(). + * @param Closure|string $callback + * @return void + */ + public static function fetched($callback) + { + static::registerModelEvent('fetched', $callback); + } + + /** + * Register a saving model event with the dispatcher. + * + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function saving($callback, $priority = 0) + { + static::registerModelEvent('saving', $callback, $priority); + } + + /** + * Register a saved model event with the dispatcher. + * + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function saved($callback, $priority = 0) + { + static::registerModelEvent('saved', $callback, $priority); + } + + /** + * Register an updating model event with the dispatcher. + * + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function updating($callback, $priority = 0) + { + static::registerModelEvent('updating', $callback, $priority); + } + + /** + * Register an updated model event with the dispatcher. + * + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function updated($callback, $priority = 0) + { + static::registerModelEvent('updated', $callback, $priority); + } + + /** + * Register a creating model event with the dispatcher. + * + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function creating($callback, $priority = 0) + { + static::registerModelEvent('creating', $callback, $priority); + } + + /** + * Register a created model event with the dispatcher. + * + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function created($callback, $priority = 0) + { + static::registerModelEvent('created', $callback, $priority); + } + + /** + * Register a deleting model event with the dispatcher. + * + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function deleting($callback, $priority = 0) + { + static::registerModelEvent('deleting', $callback, $priority); + } + + /** + * Register a deleted model event with the dispatcher. + * + * @param \Closure|string $callback + * @param int $priority + * @return void + */ + public static function deleted($callback, $priority = 0) + { + static::registerModelEvent('deleted', $callback, $priority); + } + + + /** + * afterBoot is called after the model is constructed for the first time. + */ + protected function afterBoot() + { + /** + * @event model.afterBoot + * Called after the model is booted + * + * Example usage: + * + * $model->bindEvent('model.afterBoot', function () use (\October\Rain\Halcyon\Model $model) { + * \Log::info(get_class($model) . ' has booted'); + * }); + * + */ + } + + /** + * afterInit is called after the model is constructed, a nicer version + * of overriding the __construct method. + */ + protected function afterInit() + { + /** + * @event model.afterInit + * Called after the model is initialized + * + * Example usage: + * + * $model->bindEvent('model.afterInit', function () use (\October\Rain\Halcyon\Model $model) { + * \Log::info(get_class($model) . ' has initialized'); + * }); + * + */ + } + + /** + * beforeCreate handles the "creating" model event + */ + protected function beforeCreate() + { + /** + * @event model.beforeCreate + * Called before the model is created + * + * Example usage: + * + * $model->bindEvent('model.beforeCreate', function () use (\October\Rain\Halcyon\Model $model) { + * if (!$model->isValid()) { + * throw new \Exception("Invalid Model!"); + * } + * }); + * + */ + } + + /** + * afterCreate handles the "created" model event + */ + protected function afterCreate() + { + /** + * @event model.afterCreate + * Called after the model is created + * + * Example usage: + * + * $model->bindEvent('model.afterCreate', function () use (\October\Rain\Halcyon\Model $model) { + * \Log::info("{$model->name} was created!"); + * }); + * + */ + } + + /** + * beforeUpdate handles the "updating" model event + */ + protected function beforeUpdate() + { + /** + * @event model.beforeUpdate + * Called before the model is updated + * + * Example usage: + * + * $model->bindEvent('model.beforeUpdate', function () use (\October\Rain\Halcyon\Model $model) { + * if (!$model->isValid()) { + * throw new \Exception("Invalid Model!"); + * } + * }); + * + */ + } + + /** + * afterUpdate handles the "updated" model event + */ + protected function afterUpdate() + { + /** + * @event model.afterUpdate + * Called after the model is updated + * + * Example usage: + * + * $model->bindEvent('model.afterUpdate', function () use (\October\Rain\Halcyon\Model $model) { + * if ($model->title !== $model->original['title']) { + * \Log::info("{$model->name} updated its title!"); + * } + * }); + * + */ + } + + /** + * beforeSave handles the "saving" model event + */ + protected function beforeSave() + { + /** + * @event model.beforeSave + * Called before the model is created or updated + * + * Example usage: + * + * $model->bindEvent('model.beforeSave', function () use (\October\Rain\Halcyon\Model $model) { + * if (!$model->isValid()) { + * throw new \Exception("Invalid Model!"); + * } + * }); + * + */ + } + + /** + * afterSave handles the "saved" model event + */ + protected function afterSave() + { + /** + * @event model.afterSave + * Called after the model is created or updated + * + * Example usage: + * + * $model->bindEvent('model.afterSave', function () use (\October\Rain\Halcyon\Model $model) { + * if ($model->title !== $model->original['title']) { + * \Log::info("{$model->name} updated its title!"); + * } + * }); + * + */ + } + + /** + * beforeDelete handles the "deleting" model event + */ + protected function beforeDelete() + { + /** + * @event model.beforeDelete + * Called before the model is deleted + * + * Example usage: + * + * $model->bindEvent('model.beforeDelete', function () use (\October\Rain\Halcyon\Model $model) { + * if (!$model->isAllowedToBeDeleted()) { + * throw new \Exception("You cannot delete me!"); + * } + * }); + * + */ + } + + /** + * afterDelete handles the "deleted" model event + */ + protected function afterDelete() + { + /** + * @event model.afterDelete + * Called after the model is deleted + * + * Example usage: + * + * $model->bindEvent('model.afterDelete', function () use (\October\Rain\Halcyon\Model $model) { + * \Log::info("{$model->name} was deleted"); + * }); + * + */ + } + + /** + * beforeFetch handles the "fetching" model event + */ + protected function beforeFetch() + { + /** + * @event model.beforeFetch + * Called before the model is fetched + * + * Example usage: + * + * $model->bindEvent('model.beforeFetch', function () use (\October\Rain\Halcyon\Model $model) { + * if (!\Auth::getUser()->hasAccess('fetch.this.model')) { + * throw new \Exception("You shall not pass!"); + * } + * }); + * + */ + } + + /** + * afterFetch handles the "fetched" model event + */ + protected function afterFetch() + { + /** + * @event model.afterFetch + * Called after the model is fetched + * + * Example usage: + * + * $model->bindEvent('model.afterFetch', function () use (\October\Rain\Halcyon\Model $model) { + * \Log::info("{$model->name} was retrieved from the database"); + * }); + * + */ + } +} diff --git a/src/Halcyon/Model.php b/src/Halcyon/Model.php index 6d4d64168..f16637404 100644 --- a/src/Halcyon/Model.php +++ b/src/Halcyon/Model.php @@ -6,7 +6,6 @@ use October\Rain\Halcyon\Datasource\ResolverInterface as Resolver; use Illuminate\Contracts\Support\Jsonable; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Contracts\Events\Dispatcher; use BadMethodCallException; use JsonSerializable; use ArrayAccess; @@ -21,6 +20,7 @@ class Model extends Extendable implements ArrayAccess, Arrayable, Jsonable, JsonSerializable { use \October\Rain\Support\Traits\Emitter; + use \October\Rain\Halcyon\Concerns\HasEvents; /** * @var string datasource is the data source for the model, a directory path. @@ -89,11 +89,6 @@ class Model extends Extendable implements ArrayAccess, Arrayable, Jsonable, Json */ protected $loadedFromCache = false; - /** - * @var array observables are user exposed observable events. - */ - protected $observables = []; - /** * @var bool exists indicates if the model exists. */ @@ -109,21 +104,11 @@ class Model extends Extendable implements ArrayAccess, Arrayable, Jsonable, Json */ protected static $resolver; - /** - * @var \Illuminate\Contracts\Events\Dispatcher dispatcher instance - */ - protected static $dispatcher; - /** * @var array mutatorCache for each class. */ protected static $mutatorCache = []; - /** - * @var array eventsBooted is the array of models booted events. - */ - protected static $eventsBooted = []; - /** * @var array booted models */ @@ -149,6 +134,8 @@ public function __construct(array $attributes = []) parent::__construct(); + $this->initializeModelEvent(); + $this->syncOriginal(); $this->fill($attributes); @@ -244,50 +231,6 @@ public static function clearBootedModels() static::$booted = []; } - /** - * bootNicerEvents binds some nicer events to this model, in the format of method overrides. - */ - protected function bootNicerEvents() - { - $class = get_called_class(); - - if (isset(static::$eventsBooted[$class])) { - return; - } - - $radicals = ['creat', 'sav', 'updat', 'delet', 'fetch']; - $hooks = ['before' => 'ing', 'after' => 'ed']; - - foreach ($radicals as $radical) { - foreach ($hooks as $hook => $event) { - $eventMethod = $radical . $event; // saving / saved - $method = $hook . ucfirst($radical); // beforeSave / afterSave - if ($radical !== 'fetch') { - $method .= 'e'; - } - - self::$eventMethod(function ($model) use ($method) { - $model->fireEvent('model.' . $method); - - if ($model->methodExists($method)) { - return $model->$method(); - } - }); - } - } - - // Hook to boot events - // - static::registerModelEvent('booted', function ($model) { - $model->fireEvent('model.afterBoot'); - if ($model->methodExists('afterBoot')) { - return $model->afterBoot(); - } - }); - - static::$eventsBooted[$class] = true; - } - /** * getIdAttribute is a helper for {{ page.id }} or {{ layout.id }} twig vars * Returns a semi-unique string for this object. @@ -975,215 +918,6 @@ protected function performDeleteOnModel() $this->newQuery()->delete($this->fileName); } - /** - * Create a new native event for handling beforeFetch(). - * @param Closure|string $callback - * @return void - */ - public static function fetching($callback) - { - static::registerModelEvent('fetching', $callback); - } - - /** - * Create a new native event for handling afterFetch(). - * @param Closure|string $callback - * @return void - */ - public static function fetched($callback) - { - static::registerModelEvent('fetched', $callback); - } - - /** - * Register a saving model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - * @return void - */ - public static function saving($callback, $priority = 0) - { - static::registerModelEvent('saving', $callback, $priority); - } - - /** - * Register a saved model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - * @return void - */ - public static function saved($callback, $priority = 0) - { - static::registerModelEvent('saved', $callback, $priority); - } - - /** - * Register an updating model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - * @return void - */ - public static function updating($callback, $priority = 0) - { - static::registerModelEvent('updating', $callback, $priority); - } - - /** - * Register an updated model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - * @return void - */ - public static function updated($callback, $priority = 0) - { - static::registerModelEvent('updated', $callback, $priority); - } - - /** - * Register a creating model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - * @return void - */ - public static function creating($callback, $priority = 0) - { - static::registerModelEvent('creating', $callback, $priority); - } - - /** - * Register a created model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - * @return void - */ - public static function created($callback, $priority = 0) - { - static::registerModelEvent('created', $callback, $priority); - } - - /** - * Register a deleting model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - * @return void - */ - public static function deleting($callback, $priority = 0) - { - static::registerModelEvent('deleting', $callback, $priority); - } - - /** - * Register a deleted model event with the dispatcher. - * - * @param \Closure|string $callback - * @param int $priority - * @return void - */ - public static function deleted($callback, $priority = 0) - { - static::registerModelEvent('deleted', $callback, $priority); - } - - /** - * Remove all of the event listeners for the model. - * - * @return void - */ - public static function flushEventListeners() - { - if (!isset(static::$dispatcher)) { - return; - } - - $instance = new static; - - foreach ($instance->getObservableEvents() as $event) { - static::$dispatcher->forget("halcyon.{$event}: ".get_called_class()); - } - - static::$eventsBooted = []; - } - - /** - * Register a model event with the dispatcher. - * - * @param string $event - * @param \Closure|string $callback - * @param int $priority - * @return void - */ - protected static function registerModelEvent($event, $callback, $priority = 0) - { - if (isset(static::$dispatcher)) { - $name = get_called_class(); - - static::$dispatcher->listen("halcyon.{$event}: {$name}", $callback, $priority); - } - } - - /** - * Get the observable event names. - * - * @return array - */ - public function getObservableEvents() - { - return array_merge( - [ - 'creating', 'created', 'updating', 'updated', - 'deleting', 'deleted', 'saving', 'saved', - 'fetching', 'fetched' - ], - $this->observables - ); - } - - /** - * Set the observable event names. - * - * @param array $observables - * @return $this - */ - public function setObservableEvents(array $observables) - { - $this->observables = $observables; - - return $this; - } - - /** - * Add an observable event name. - * - * @param array|mixed $observables - * @return void - */ - public function addObservableEvents($observables) - { - $observables = is_array($observables) ? $observables : func_get_args(); - - $this->observables = array_unique(array_merge($this->observables, $observables)); - } - - /** - * Remove an observable event name. - * - * @param array|mixed $observables - * @return void - */ - public function removeObservableEvents($observables) - { - $observables = is_array($observables) ? $observables : func_get_args(); - - $this->observables = array_diff($this->observables, $observables); - } - /** * Update the model in the database. * @@ -1306,7 +1040,7 @@ protected function performInsert(Builder $query, array $options = []) } // Ensure the settings attribute is passed through so this distinction - // is recognised, mainly by the processor. + // is recognized, mainly by the processor. $attributes = $this->attributesToArray(); $query->insert($attributes); @@ -1321,29 +1055,6 @@ protected function performInsert(Builder $query, array $options = []) return true; } - /** - * Fire the given event for the model. - * - * @param string $event - * @param bool $halt - * @return mixed - */ - protected function fireModelEvent($event, $halt = true) - { - if (!isset(static::$dispatcher)) { - return true; - } - - // We will append the names of the class to the event to distinguish it from - // other model events that are fired, allowing us to listen on each model - // event set individually instead of catching event for all the models. - $event = "halcyon.{$event}: ".get_class($this); - - $method = $halt ? 'until' : 'dispatch'; - - return static::$dispatcher->$method($event, $this); - } - /** * Get a new query builder for the object * @return \October\Rain\Halcyon\Builder @@ -1466,37 +1177,6 @@ public static function unsetDatasourceResolver() static::$resolver = null; } - /** - * getEventDispatcher instance. - * - * @return \Illuminate\Contracts\Events\Dispatcher - */ - public static function getEventDispatcher() - { - return static::$dispatcher; - } - - /** - * setEventDispatcher instance. - * - * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher - * @return void - */ - public static function setEventDispatcher(Dispatcher $dispatcher) - { - static::$dispatcher = $dispatcher; - } - - /** - * unsetEventDispatcher for models. - * - * @return void - */ - public static function unsetEventDispatcher() - { - static::$dispatcher = null; - } - /** * getCacheManager instance. * @@ -1740,5 +1420,7 @@ public function __wakeup() $this->initializeTraits(); $this->bootNicerEvents(); + + $this->initializeModelEvent(); } } From bd8435d4be70a6e1c595752a5e9eb97f6110c199 Mon Sep 17 00:00:00 2001 From: October CMS Date: Fri, 15 Sep 2023 14:15:44 +1000 Subject: [PATCH 14/74] Micro optimization --- src/Database/Concerns/HasEvents.php | 5 +---- src/Halcyon/Concerns/HasEvents.php | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Database/Concerns/HasEvents.php b/src/Database/Concerns/HasEvents.php index ef76fee1b..86c1a2fb6 100644 --- a/src/Database/Concerns/HasEvents.php +++ b/src/Database/Concerns/HasEvents.php @@ -41,10 +41,7 @@ protected function bootNicerEvents() foreach ($nicerEvents as $eventMethod => $method) { self::$eventMethod(function ($model) use ($method) { $model->fireEvent('model.' . $method); - - if ($model->methodExists($method)) { - return $model->$method(); - } + return $model->$method(); }); } diff --git a/src/Halcyon/Concerns/HasEvents.php b/src/Halcyon/Concerns/HasEvents.php index 1732d3fd7..8bbb4570b 100644 --- a/src/Halcyon/Concerns/HasEvents.php +++ b/src/Halcyon/Concerns/HasEvents.php @@ -52,10 +52,7 @@ protected function bootNicerEvents() foreach ($nicerEvents as $eventMethod => $method) { self::$eventMethod(function ($model) use ($method) { $model->fireEvent('model.' . $method); - - if ($model->methodExists($method)) { - return $model->$method(); - } + return $model->$method(); }); } From abe3684962010278b8814aff5b2b812bb05ba8e8 Mon Sep 17 00:00:00 2001 From: October CMS Date: Fri, 15 Sep 2023 14:18:23 +1000 Subject: [PATCH 15/74] =?UTF-8?q?Refactor=20get=5Fcalled=5Fclass=20?= =?UTF-8?q?=E2=86=92=20static::class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Database/Concerns/HasEvents.php | 6 ++---- src/Database/Concerns/HasRelationships.php | 8 ++++---- src/Halcyon/Concerns/HasEvents.php | 8 +++----- src/Halcyon/Traits/Validation.php | 4 ++-- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Database/Concerns/HasEvents.php b/src/Database/Concerns/HasEvents.php index 86c1a2fb6..8a34a7d77 100644 --- a/src/Database/Concerns/HasEvents.php +++ b/src/Database/Concerns/HasEvents.php @@ -18,9 +18,7 @@ trait HasEvents */ protected function bootNicerEvents() { - $class = get_called_class(); - - if (isset(static::$eventsBooted[$class])) { + if (isset(static::$eventsBooted[static::class])) { return; } @@ -49,7 +47,7 @@ protected function bootNicerEvents() $this->fireEvent('model.afterBoot'); $this->afterBoot(); - static::$eventsBooted[$class] = true; + static::$eventsBooted[static::class] = true; } /** diff --git a/src/Database/Concerns/HasRelationships.php b/src/Database/Concerns/HasRelationships.php index 001d46af2..db9dc3fd1 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -330,7 +330,7 @@ protected function handleRelation($relationName) throw new InvalidArgumentException(sprintf( "Relation '%s' on model '%s' should have at least a classname.", $relationName, - get_called_class() + static::class )); } @@ -338,7 +338,7 @@ protected function handleRelation($relationName) throw new InvalidArgumentException(sprintf( "Relation '%s' on model '%s' is a morphTo relation and should not contain additional arguments.", $relationName, - get_called_class() + static::class )); } @@ -393,7 +393,7 @@ protected function handleRelation($relationName) break; default: - throw new InvalidArgumentException(sprintf("There is no such relation type known as '%s' on model '%s'.", $relationType, get_called_class())); + throw new InvalidArgumentException(sprintf("There is no such relation type known as '%s' on model '%s'.", $relationType, static::class)); } // Relation hook event @@ -430,7 +430,7 @@ protected function validateRelationArgs($relationName, $optional, $required = [] throw new InvalidArgumentException(sprintf( 'Relation "%s" on model "%s" should contain the following key(s): %s', $relationName, - get_called_class(), + static::class, implode(', ', $missingRequired) )); } diff --git a/src/Halcyon/Concerns/HasEvents.php b/src/Halcyon/Concerns/HasEvents.php index 8bbb4570b..5539cc909 100644 --- a/src/Halcyon/Concerns/HasEvents.php +++ b/src/Halcyon/Concerns/HasEvents.php @@ -30,9 +30,7 @@ trait HasEvents */ protected function bootNicerEvents() { - $class = get_called_class(); - - if (isset(static::$eventsBooted[$class])) { + if (isset(static::$eventsBooted[static::class])) { return; } @@ -60,7 +58,7 @@ protected function bootNicerEvents() $this->fireEvent('model.afterBoot'); $this->afterBoot(); - static::$eventsBooted[$class] = true; + static::$eventsBooted[static::class] = true; } /** @@ -84,7 +82,7 @@ public static function flushEventListeners() $instance = new static; foreach ($instance->getObservableEvents() as $event) { - static::$dispatcher->forget("halcyon.{$event}: ".get_called_class()); + static::$dispatcher->forget("halcyon.{$event}: ".static::class); } static::$eventsBooted = []; diff --git a/src/Halcyon/Traits/Validation.php b/src/Halcyon/Traits/Validation.php index 2f5138267..7e890cd7e 100644 --- a/src/Halcyon/Traits/Validation.php +++ b/src/Halcyon/Traits/Validation.php @@ -48,8 +48,8 @@ trait Validation */ public static function bootValidation() { - if (!property_exists(get_called_class(), 'rules')) { - throw new Exception(sprintf('You must define a $rules property in %s to use the Validation trait.', get_called_class())); + if (!property_exists(static::class, 'rules')) { + throw new Exception(sprintf('You must define a $rules property in %s to use the Validation trait.', static::class)); } static::extend(function ($model) { From 277795726edce50740685360955d037730010a3a Mon Sep 17 00:00:00 2001 From: October CMS Date: Fri, 15 Sep 2023 14:22:02 +1000 Subject: [PATCH 16/74] =?UTF-8?q?Refactor=20get=5Fclass($this)=20=E2=86=92?= =?UTF-8?q?=20static::class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Database/Traits/Encryptable.php | 2 +- src/Database/Traits/Hashable.php | 2 +- src/Database/Traits/Multisite.php | 2 +- src/Database/Traits/NestedTree.php | 4 ++-- src/Database/Traits/Nullable.php | 2 +- src/Database/Traits/Purgeable.php | 2 +- src/Database/Traits/Revisionable.php | 4 ++-- src/Database/Traits/SimpleTree.php | 4 ++-- src/Database/Traits/Sluggable.php | 2 +- src/Database/Traits/Validation.php | 2 +- src/Extension/ExtendableTrait.php | 10 +++++----- src/Extension/ExtensionTrait.php | 2 +- src/Halcyon/Builder.php | 2 +- src/Halcyon/Model.php | 2 +- src/Scaffold/GeneratorCommandBase.php | 3 +-- 15 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/Database/Traits/Encryptable.php b/src/Database/Traits/Encryptable.php index 21f551d53..b30495699 100644 --- a/src/Database/Traits/Encryptable.php +++ b/src/Database/Traits/Encryptable.php @@ -31,7 +31,7 @@ public function initializeEncryptable() if (!is_array($this->encryptable)) { throw new Exception(sprintf( 'The $encryptable property in %s must be an array to use the Encryptable trait.', - get_class($this) + static::class )); } diff --git a/src/Database/Traits/Hashable.php b/src/Database/Traits/Hashable.php index f91356351..7913d7c8e 100644 --- a/src/Database/Traits/Hashable.php +++ b/src/Database/Traits/Hashable.php @@ -30,7 +30,7 @@ public function initializeHashable() if (!is_array($this->hashable)) { throw new Exception(sprintf( 'The $hashable property in %s must be an array to use the Hashable trait.', - get_class($this) + static::class )); } diff --git a/src/Database/Traits/Multisite.php b/src/Database/Traits/Multisite.php index 486f6e080..aecddffe5 100644 --- a/src/Database/Traits/Multisite.php +++ b/src/Database/Traits/Multisite.php @@ -41,7 +41,7 @@ public function initializeMultisite() if (!is_array($this->propagatable)) { throw new Exception(sprintf( 'The $propagatable property in %s must be an array to use the Multisite trait.', - get_class($this) + static::class )); } diff --git a/src/Database/Traits/NestedTree.php b/src/Database/Traits/NestedTree.php index 31946dd24..7ffe7dd07 100644 --- a/src/Database/Traits/NestedTree.php +++ b/src/Database/Traits/NestedTree.php @@ -92,13 +92,13 @@ public function initializeNestedTree() { // Define relationships $this->hasMany['children'] = [ - get_class($this), + static::class, 'key' => $this->getParentColumnName(), 'replicate' => false ]; $this->belongsTo['parent'] = [ - get_class($this), + static::class, 'key' => $this->getParentColumnName(), 'replicate' => false ]; diff --git a/src/Database/Traits/Nullable.php b/src/Database/Traits/Nullable.php index 86e611b04..9f5cc5495 100644 --- a/src/Database/Traits/Nullable.php +++ b/src/Database/Traits/Nullable.php @@ -24,7 +24,7 @@ public function initializeNullable() if (!is_array($this->nullable)) { throw new Exception(sprintf( 'The $nullable property in %s must be an array to use the Nullable trait.', - get_class($this) + static::class )); } diff --git a/src/Database/Traits/Purgeable.php b/src/Database/Traits/Purgeable.php index b7604f67d..dc4737d09 100644 --- a/src/Database/Traits/Purgeable.php +++ b/src/Database/Traits/Purgeable.php @@ -29,7 +29,7 @@ public function initializePurgeable() if (!is_array($this->purgeable)) { throw new Exception(sprintf( 'The $purgeable property in %s must be an array to use the Purgeable trait.', - get_class($this) + static::class )); } diff --git a/src/Database/Traits/Revisionable.php b/src/Database/Traits/Revisionable.php index 965abe7c6..e7bda43db 100644 --- a/src/Database/Traits/Revisionable.php +++ b/src/Database/Traits/Revisionable.php @@ -44,7 +44,7 @@ public function initializeRevisionable() if (!is_array($this->revisionable)) { throw new Exception(sprintf( 'The $revisionable property in %s must be an array to use the Revisionable trait.', - get_class($this) + static::class )); } @@ -110,7 +110,7 @@ public function revisionableAfterDelete() $softDeletes = in_array( \October\Rain\Database\Traits\SoftDelete::class, - class_uses_recursive(get_class($this)) + class_uses_recursive(static::class) ); if (!$softDeletes) { diff --git a/src/Database/Traits/SimpleTree.php b/src/Database/Traits/SimpleTree.php index ecdf2eb35..181525efb 100644 --- a/src/Database/Traits/SimpleTree.php +++ b/src/Database/Traits/SimpleTree.php @@ -48,13 +48,13 @@ public function initializeSimpleTree() { // Define relationships $this->hasMany['children'] = [ - get_class($this), + static::class, 'key' => $this->getParentColumnName(), 'replicate' => false ]; $this->belongsTo['parent'] = [ - get_class($this), + static::class, 'key' => $this->getParentColumnName(), 'replicate' => false ]; diff --git a/src/Database/Traits/Sluggable.php b/src/Database/Traits/Sluggable.php index 1129e2e21..2333547f6 100644 --- a/src/Database/Traits/Sluggable.php +++ b/src/Database/Traits/Sluggable.php @@ -26,7 +26,7 @@ public function initializeSluggable() if (!is_array($this->slugs)) { throw new Exception(sprintf( 'The $slugs property in %s must be an array to use the Sluggable trait.', - get_class($this) + static::class )); } diff --git a/src/Database/Traits/Validation.php b/src/Database/Traits/Validation.php index 46f6abed5..afaac4a6d 100644 --- a/src/Database/Traits/Validation.php +++ b/src/Database/Traits/Validation.php @@ -65,7 +65,7 @@ public function initializeValidation() if (!is_array($this->rules)) { throw new Exception(sprintf( 'The $rules property in %s must be an array to use the Validation trait.', - get_class($this) + static::class )); } diff --git a/src/Extension/ExtendableTrait.php b/src/Extension/ExtendableTrait.php index 6aaaab6f4..45f292765 100644 --- a/src/Extension/ExtendableTrait.php +++ b/src/Extension/ExtendableTrait.php @@ -45,7 +45,7 @@ trait ExtendableTrait public function extendableConstruct() { // Apply init callbacks - $classes = array_merge([get_class($this)], class_parents($this)); + $classes = array_merge([static::class], class_parents($this)); foreach ($classes as $class) { if (isset(Container::$classCallbacks[$class]) && is_array(Container::$classCallbacks[$class])) { foreach (Container::$classCallbacks[$class] as $callback) { @@ -122,7 +122,7 @@ protected function extensionExtractImplements(): array $uses = $this->implement; } else { - throw new Exception(sprintf('Class %s contains an invalid $implement value', get_class($this))); + throw new Exception(sprintf('Class %s contains an invalid $implement value', static::class)); } foreach ($uses as &$use) { @@ -225,7 +225,7 @@ public function extendClassWith($extensionName) if (isset($this->extensionData['extensions'][$extensionName])) { throw new Exception(sprintf( 'Class %s has already been extended with %s', - get_class($this), + static::class, $extensionName )); } @@ -480,7 +480,7 @@ public function extendableSet($name, $value) // if (!$found) { // throw new BadMethodCallException(sprintf( // 'Call to undefined property %s::%s', - // get_class($this), + // static::class, // $name // )); // } @@ -511,7 +511,7 @@ public function extendableCall($name, $params = null) throw new BadMethodCallException(sprintf( 'Call to undefined method %s::%s()', - get_class($this), + static::class, $name )); } diff --git a/src/Extension/ExtensionTrait.php b/src/Extension/ExtensionTrait.php index 6aa966681..38bc11270 100644 --- a/src/Extension/ExtensionTrait.php +++ b/src/Extension/ExtensionTrait.php @@ -27,7 +27,7 @@ trait ExtensionTrait */ public function extensionApplyInitCallbacks() { - $classes = array_merge([get_class($this)], class_parents($this)); + $classes = array_merge([static::class], class_parents($this)); foreach ($classes as $class) { if (isset(Container::$extensionCallbacks[$class]) && is_array(Container::$extensionCallbacks[$class])) { foreach (Container::$extensionCallbacks[$class] as $callback) { diff --git a/src/Halcyon/Builder.php b/src/Halcyon/Builder.php index 79a66630e..2a8a85d54 100644 --- a/src/Halcyon/Builder.php +++ b/src/Halcyon/Builder.php @@ -735,7 +735,7 @@ protected function processInitCacheData($data) */ public function __call($method, $parameters) { - $className = get_class($this); + $className = static::class; throw new BadMethodCallException("Call to undefined method {$className}::{$method}()"); } diff --git a/src/Halcyon/Model.php b/src/Halcyon/Model.php index f16637404..617e3a0cc 100644 --- a/src/Halcyon/Model.php +++ b/src/Halcyon/Model.php @@ -1224,7 +1224,7 @@ public static function initCacheItem(&$item) */ public function getMutatedAttributes() { - $class = get_class($this); + $class = static::class; if (!isset(static::$mutatorCache[$class])) { static::cacheMutatedAttributes($class); diff --git a/src/Scaffold/GeneratorCommandBase.php b/src/Scaffold/GeneratorCommandBase.php index 55c0cd13a..6e5c9db8e 100644 --- a/src/Scaffold/GeneratorCommandBase.php +++ b/src/Scaffold/GeneratorCommandBase.php @@ -211,8 +211,7 @@ protected function getDestinationPath(): string */ protected function getSourcePath(): string { - $className = get_class($this); - $class = new ReflectionClass($className); + $class = new ReflectionClass(static::class); return dirname($class->getFileName()); } From 4d377604163c02564d965c82e644f99042b4a6a3 Mon Sep 17 00:00:00 2001 From: October CMS Date: Fri, 29 Sep 2023 17:12:52 +1000 Subject: [PATCH 17/74] Deprecate --- src/Support/Facades/Site.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Support/Facades/Site.php b/src/Support/Facades/Site.php index 7b8b5b0a1..8b4c9c236 100644 --- a/src/Support/Facades/Site.php +++ b/src/Support/Facades/Site.php @@ -29,8 +29,6 @@ class Site extends Facade */ protected static function getFacadeAccessor() { - // @deprecated use below - // return 'system.sites'; - return 'site.manager'; + return 'system.sites'; } } From 4d6ee1ad017333caf08971b0d65ecc8da130cd13 Mon Sep 17 00:00:00 2001 From: October CMS Date: Wed, 4 Oct 2023 17:45:35 +1100 Subject: [PATCH 18/74] XT add --- src/Foundation/Application.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Foundation/Application.php b/src/Foundation/Application.php index c81b75f16..845ea5bea 100644 --- a/src/Foundation/Application.php +++ b/src/Foundation/Application.php @@ -455,4 +455,14 @@ public function getNamespace() { return 'App\\'; } + + /** + * extend to allow undecorated extension without returning an object + */ + public function extend($abstract, Closure $callback) + { + parent::extend($abstract, function(...$args) use ($callback) { + return $callback(...$args) ?? $args[0]; + }); + } } From b92fd6e350e31e45b2f0171df4fd7d12ec592bc1 Mon Sep 17 00:00:00 2001 From: October CMS Date: Thu, 5 Oct 2023 08:35:08 +1100 Subject: [PATCH 19/74] Replace XT with new method This doesn't call rebind since it is only modifying the object --- src/Foundation/Application.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Foundation/Application.php b/src/Foundation/Application.php index 845ea5bea..b7c8fafa7 100644 --- a/src/Foundation/Application.php +++ b/src/Foundation/Application.php @@ -457,12 +457,14 @@ public function getNamespace() } /** - * extend to allow undecorated extension without returning an object + * extendInstance is useful for extending singletons regardless of their execution */ - public function extend($abstract, Closure $callback) + public function extendInstance($abstract, Closure $callback) { - parent::extend($abstract, function(...$args) use ($callback) { - return $callback(...$args) ?? $args[0]; - }); + $this->afterResolving($abstract, $callback); + + if ($this->resolved($abstract)) { + $callback($this->make($abstract), $this); + } } } From 275df65d4cf4605976bdda235eb3b8a5abbe5e09 Mon Sep 17 00:00:00 2001 From: October CMS Date: Mon, 9 Oct 2023 11:20:02 +1100 Subject: [PATCH 20/74] =?UTF-8?q?field=20=E2=86=92=20property?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For consistency, since these functions don't appear to be used anywhere --- src/Extension/ExtensionTrait.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Extension/ExtensionTrait.php b/src/Extension/ExtensionTrait.php index 38bc11270..adb6096b7 100644 --- a/src/Extension/ExtensionTrait.php +++ b/src/Extension/ExtensionTrait.php @@ -18,8 +18,8 @@ trait ExtensionTrait * @var array extensionHidden are properties and methods that cannot be accessed. */ protected $extensionHidden = [ - 'fields' => [], - 'methods' => ['extensionIsHiddenField', 'extensionIsHiddenMethod'] + 'methods' => ['extensionIsHiddenProperty', 'extensionIsHiddenMethod'], + 'properties' => [] ]; /** @@ -55,14 +55,6 @@ public static function extensionExtendCallback($callback) Container::$extensionCallbacks[$class][] = $callback; } - /** - * extensionHideField - */ - protected function extensionHideField($name) - { - $this->extensionHidden['fields'][] = $name; - } - /** * extensionHideMethod */ @@ -72,11 +64,11 @@ protected function extensionHideMethod($name) } /** - * extensionIsHiddenField + * extensionHideProperty */ - public function extensionIsHiddenField($name) + protected function extensionHideProperty($name) { - return in_array($name, $this->extensionHidden['fields']); + $this->extensionHidden['properties'][] = $name; } /** @@ -87,6 +79,14 @@ public function extensionIsHiddenMethod($name) return in_array($name, $this->extensionHidden['methods']); } + /** + * extensionIsHiddenProperty + */ + public function extensionIsHiddenProperty($name) + { + return in_array($name, $this->extensionHidden['properties']); + } + /** * getCalledExtensionClass */ From 7d01a39afbbf1e845e713a760b3c5442e3fe81f8 Mon Sep 17 00:00:00 2001 From: October CMS Date: Thu, 19 Oct 2023 11:45:09 +1100 Subject: [PATCH 21/74] Adds defaultable trait, currency helper --- helpers/Currency.php | 8 ++++ src/Database/Traits/Defaultable.php | 61 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 helpers/Currency.php create mode 100644 src/Database/Traits/Defaultable.php diff --git a/helpers/Currency.php b/helpers/Currency.php new file mode 100644 index 000000000..fb95f7632 --- /dev/null +++ b/helpers/Currency.php @@ -0,0 +1,8 @@ +bindEvent('model.afterSave', [$this, 'defaultableAfterSave']); + } + + /** + * defaultableAfterSave + */ + public function defaultableAfterSave() + { + if ($this->is_default) { + $this->makeDefault(); + } + } + + /** + * makeDefault + */ + public function makeDefault() + { + $this->newQuery()->where('id', $this->id)->update(['is_default' => true]); + $this->newQuery()->where('id', '<>', $this->id)->update(['is_default' => false]); + } + + /** + * getDefault returns the default product type. + */ + public static function getDefault() + { + if (static::$defaultableCache !== null) { + return static::$defaultableCache; + } + + $defaultType = static::where('is_default', true)->first(); + + // If no default is found, find the first record and make it the default. + if (!$defaultType && ($defaultType = static::first())) { + $defaultType->makeDefault(); + } + + return static::$defaultableCache = $defaultType; + } +} From 3a84895ad2223e37c29bbe1633e4b36d9091d0fb Mon Sep 17 00:00:00 2001 From: October CMS Date: Thu, 26 Oct 2023 14:44:10 +1100 Subject: [PATCH 22/74] Adds "Done" events to models This lets ExpandoModel use beforeSaveDone to make its adjustments, and the parent model can use beforeSave() to manipulate attributes. Previously it was locked out since the serialization already occurred via saveInternal. --- src/Database/Concerns/HasEvents.php | 6 ++++-- src/Database/ExpandoModel.php | 9 ++++----- src/Halcyon/Concerns/HasEvents.php | 6 ++++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Database/Concerns/HasEvents.php b/src/Database/Concerns/HasEvents.php index 8a34a7d77..b86ebf9cc 100644 --- a/src/Database/Concerns/HasEvents.php +++ b/src/Database/Concerns/HasEvents.php @@ -38,8 +38,10 @@ protected function bootNicerEvents() foreach ($nicerEvents as $eventMethod => $method) { self::$eventMethod(function ($model) use ($method) { - $model->fireEvent('model.' . $method); - return $model->$method(); + $model->fireEvent("model.{$method}"); + $result = $model->$method(); + $model->fireEvent("model.{$method}Done"); + return $result; }); } diff --git a/src/Database/ExpandoModel.php b/src/Database/ExpandoModel.php index e0f383c2a..47da24fd6 100644 --- a/src/Database/ExpandoModel.php +++ b/src/Database/ExpandoModel.php @@ -30,14 +30,13 @@ public function __construct(array $attributes = []) $this->bindEvent('model.afterSave', [$this, 'expandoAfterSave']); - // Process attributes last for traits with attribute modifiers - $this->bindEvent('model.saveInternal', [$this, 'expandoSaveInternal'], -1); + $this->bindEvent('model.beforeSaveDone', [$this, 'expandoBeforeSaveDone']); $this->addJsonable($this->expandoColumn); } /** - * afterModelFetch constructor event + * expandoAfterFetch constructor event */ public function expandoAfterFetch() { @@ -47,9 +46,9 @@ public function expandoAfterFetch() } /** - * saveModelInternal constructor event + * expandoBeforeSaveDone constructor event */ - public function expandoSaveInternal() + public function expandoBeforeSaveDone() { $this->{$this->expandoColumn} = array_diff_key( $this->attributes, diff --git a/src/Halcyon/Concerns/HasEvents.php b/src/Halcyon/Concerns/HasEvents.php index 5539cc909..f60c01f3d 100644 --- a/src/Halcyon/Concerns/HasEvents.php +++ b/src/Halcyon/Concerns/HasEvents.php @@ -49,8 +49,10 @@ protected function bootNicerEvents() foreach ($nicerEvents as $eventMethod => $method) { self::$eventMethod(function ($model) use ($method) { - $model->fireEvent('model.' . $method); - return $model->$method(); + $model->fireEvent("model.{$method}"); + $result = $model->$method(); + $model->fireEvent("model.{$method}Done"); + return $result; }); } From 723a2c1661d65c21c0e3ecb8ef5a5531a8f5adfa Mon Sep 17 00:00:00 2001 From: October CMS Date: Thu, 26 Oct 2023 15:13:03 +1100 Subject: [PATCH 23/74] Make purged attributes available during model events This makes getOriginalPurgeValue somewhat deprecated --- src/Database/ExpandoModel.php | 3 ++- src/Database/Traits/Purgeable.php | 36 ++++++++++++++++--------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Database/ExpandoModel.php b/src/Database/ExpandoModel.php index 47da24fd6..14ca00621 100644 --- a/src/Database/ExpandoModel.php +++ b/src/Database/ExpandoModel.php @@ -30,7 +30,8 @@ public function __construct(array $attributes = []) $this->bindEvent('model.afterSave', [$this, 'expandoAfterSave']); - $this->bindEvent('model.beforeSaveDone', [$this, 'expandoBeforeSaveDone']); + // Process attributes last for traits with attribute modifiers + $this->bindEvent('model.beforeSaveDone', [$this, 'expandoBeforeSaveDone'], -1); $this->addJsonable($this->expandoColumn); } diff --git a/src/Database/Traits/Purgeable.php b/src/Database/Traits/Purgeable.php index dc4737d09..4a14a25ea 100644 --- a/src/Database/Traits/Purgeable.php +++ b/src/Database/Traits/Purgeable.php @@ -33,10 +33,7 @@ public function initializePurgeable() )); } - // Remove any purge attributes from the data set - $this->bindEvent('model.saveInternal', function () { - $this->purgeAttributes(); - }); + $this->bindEvent('model.beforeSaveDone', [$this, 'purgeAttributes']); } /** @@ -53,28 +50,29 @@ public function addPurgeable($attributes = null) /** * purgeAttributes removes purged attributes from the dataset, used before saving. - * @param $attributes mixed Attribute(s) to purge, if unspecified, $purgable property is used - * @return array Current attribute set + * Specify attributesToPurge, if unspecified, $purgeable property is used + * @param mixed $attributes + * @return array */ public function purgeAttributes($attributesToPurge = null) { - if ($attributesToPurge !== null) { - $purgeable = is_array($attributesToPurge) ? $attributesToPurge : [$attributesToPurge]; + if ($attributesToPurge === null) { + $purgeable = $this->getPurgeableAttributes(); } else { - $purgeable = $this->getPurgeableAttributes(); + $purgeable = (array) $attributesToPurge; } $attributes = $this->getAttributes(); + $cleanAttributes = array_diff_key($attributes, array_flip($purgeable)); + $originalAttributes = array_diff_key($attributes, $cleanAttributes); - if (is_array($this->originalPurgeableValues)) { - $this->originalPurgeableValues = array_merge($this->originalPurgeableValues, $originalAttributes); - } - else { - $this->originalPurgeableValues = $originalAttributes; - } + $this->originalPurgeableValues = array_merge( + $this->originalPurgeableValues, + $originalAttributes + ); return $this->attributes = $cleanAttributes; } @@ -100,7 +98,8 @@ public function getOriginalPurgeValues() */ public function getOriginalPurgeValue($attribute) { - return $this->originalPurgeableValues[$attribute] ?? null; + return $this->attributes[$attribute] + ?? ($this->originalPurgeableValues[$attribute] ?? null); } /** @@ -108,6 +107,9 @@ public function getOriginalPurgeValue($attribute) */ public function restorePurgedValues() { - $this->attributes = array_merge($this->getAttributes(), $this->originalPurgeableValues); + $this->attributes = array_merge( + $this->getAttributes(), + $this->originalPurgeableValues + ); } } From d0abdf0b265d7cd0ccdd69c766c8464c2eda74a5 Mon Sep 17 00:00:00 2001 From: October CMS Date: Thu, 26 Oct 2023 15:18:10 +1100 Subject: [PATCH 24/74] Make nullable attributes available during model events --- src/Database/Traits/Nullable.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Database/Traits/Nullable.php b/src/Database/Traits/Nullable.php index 9f5cc5495..1df12dc91 100644 --- a/src/Database/Traits/Nullable.php +++ b/src/Database/Traits/Nullable.php @@ -28,15 +28,11 @@ public function initializeNullable() )); } - $this->bindEvent('model.beforeSave', function () { - $this->nullableBeforeSave(); - }); + $this->bindEvent('model.beforeSaveDone', [$this, 'nullableBeforeSave']); } /** * addNullable attribute to the nullable attributes list - * @param array|string|null $attributes - * @return void */ public function addNullable($attributes = null) { @@ -47,10 +43,8 @@ public function addNullable($attributes = null) /** * checkNullableValue checks if the supplied value is empty, excluding zero. - * @param string $value Value to check - * @return bool */ - public function checkNullableValue($value) + public function checkNullableValue($value): bool { if ($value === 0 || $value === '0' || $value === 0.0 || $value === false) { return false; From 58eeccf7abc6c42ca67956c67958509cdeaf8b40 Mon Sep 17 00:00:00 2001 From: October CMS Date: Thu, 26 Oct 2023 15:39:23 +1100 Subject: [PATCH 25/74] Refactor to a dedicated event instead beforeSaveDone was coming before beforeCreate/beforeUpdate, so, not really "done" at this point --- src/Database/Concerns/HasEvents.php | 15 +++++++++++---- src/Halcyon/Concerns/HasEvents.php | 6 ++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Database/Concerns/HasEvents.php b/src/Database/Concerns/HasEvents.php index b86ebf9cc..d34c9c82f 100644 --- a/src/Database/Concerns/HasEvents.php +++ b/src/Database/Concerns/HasEvents.php @@ -37,14 +37,21 @@ protected function bootNicerEvents() ]; foreach ($nicerEvents as $eventMethod => $method) { - self::$eventMethod(function ($model) use ($method) { + self::registerModelEvent($eventMethod, function ($model) use ($method) { $model->fireEvent("model.{$method}"); - $result = $model->$method(); - $model->fireEvent("model.{$method}Done"); - return $result; + return $model->$method(); }); } + // Hooks for late stage attribute changes + self::registerModelEvent('creating', function ($model) { + $model->fireEvent('model.beforeSaveDone'); + }); + + self::registerModelEvent('updating', function ($model) { + $model->fireEvent('model.beforeSaveDone'); + }); + // Boot event $this->fireEvent('model.afterBoot'); $this->afterBoot(); diff --git a/src/Halcyon/Concerns/HasEvents.php b/src/Halcyon/Concerns/HasEvents.php index f60c01f3d..185fae8cf 100644 --- a/src/Halcyon/Concerns/HasEvents.php +++ b/src/Halcyon/Concerns/HasEvents.php @@ -48,11 +48,9 @@ protected function bootNicerEvents() ]; foreach ($nicerEvents as $eventMethod => $method) { - self::$eventMethod(function ($model) use ($method) { + self::registerModelEvent($eventMethod, function ($model) use ($method) { $model->fireEvent("model.{$method}"); - $result = $model->$method(); - $model->fireEvent("model.{$method}Done"); - return $result; + return $model->$method(); }); } From 1ffd793d25a05aa97b86bce7bf465b10d1b56ce0 Mon Sep 17 00:00:00 2001 From: October CMS Date: Thu, 26 Oct 2023 17:54:32 +1100 Subject: [PATCH 26/74] Rem jsonable check This no longer throws an error internally since Laravel will automatically json encode the attribute, it now only becomes a problem when reading the value back, where the developer can use $jsonable or casts --- src/Database/Model.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Database/Model.php b/src/Database/Model.php index 48be9c88a..8a8af0a11 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -376,13 +376,6 @@ protected function saveInternal($options = []) return false; } - // Validate attributes before trying to save - foreach ($this->attributes as $attribute => $value) { - if (is_array($value)) { - throw new Exception(sprintf('Unexpected type of array when attempting to save attribute "%s", try adding it to the $jsonable property.', $attribute)); - } - } - // Apply pre deferred bindings if ($this->sessionKey !== null) { $this->commitDeferredBefore($this->sessionKey); From 3bf277f2cc397a0fa12ac6ac107e4f4bace487b0 Mon Sep 17 00:00:00 2001 From: October CMS Date: Tue, 31 Oct 2023 10:10:42 +1100 Subject: [PATCH 27/74] Adds selectOptions to html builder This is useful for updating select options via AJAX without replacing the element --- src/Html/FormBuilder.php | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Html/FormBuilder.php b/src/Html/FormBuilder.php index b2b674dcc..3831d9467 100644 --- a/src/Html/FormBuilder.php +++ b/src/Html/FormBuilder.php @@ -462,6 +462,12 @@ public function select($name, $list = [], $selected = null, $options = []) $list = ['' => $options['emptyOption']] + $list; } + $selectOptions = false; + if (array_key_exists('selectOptions', $options)) { + $selectOptions = $options['selectOptions'] === true; + unset($options['selectOptions']); + } + // When building a select box the "value" attribute is really the selected one // so we will use that when checking the model or session for a value which // should provide a convenient method of re-populating the forms on post. @@ -489,7 +495,20 @@ public function select($name, $list = [], $selected = null, $options = []) $list = implode('', $html); - return "{$list}"; + return $selectOptions ? $list : "{$list}"; + } + + /** + * selectOptions only renders the options inside a select. + * @param string $name + * @param array $list + * @param string $selected + * @param array $options + * @return string + */ + public function selectOptions($name, $list = [], $selected = null, $options = []) + { + return $this->select($name, $list, $selected, ['selectOptions' => true] + $options); } /** From 5eeeb03a00076ca636842adb23f303bda9aaed54 Mon Sep 17 00:00:00 2001 From: October CMS Date: Mon, 6 Nov 2023 11:59:46 +1100 Subject: [PATCH 28/74] Simply preserve the table name in the rule --- src/Database/Traits/Validation.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Database/Traits/Validation.php b/src/Database/Traits/Validation.php index e782e1764..21137ccac 100644 --- a/src/Database/Traits/Validation.php +++ b/src/Database/Traits/Validation.php @@ -441,14 +441,14 @@ protected function processValidationRules($rules) continue; } // Remove primary key unique validation rule if the model already exists - if (starts_with($rulePart, 'unique') && $this->exists) { + if (str_starts_with($rulePart, 'unique') && $this->exists) { $ruleParts[$key] = $this->processValidationUniqueRule($rulePart, $field); } // Look for required:create and required:update rules - elseif (starts_with($rulePart, 'required:create') && $this->exists) { + elseif (str_starts_with($rulePart, 'required:create') && $this->exists) { unset($ruleParts[$key]); } - elseif (starts_with($rulePart, 'required:update') && !$this->exists) { + elseif (str_starts_with($rulePart, 'required:update') && !$this->exists) { unset($ruleParts[$key]); } } @@ -494,7 +494,7 @@ protected function processValidationUniqueRule($definition, $fieldName) [$ruleName, $ruleDefinition] = array_pad(explode(':', $definition, 2), 2, ''); [$tableName, $column, $key, $keyName, $whereColumn, $whereValue] = array_pad(explode(',', $ruleDefinition, 6), 6, null); - $tableName = str_contains($tableName, '.') ? $tableName : $this->getTable(); + $tableName = $tableName ?: $this->getTable(); $column = $column ?: $fieldName; $key = $keyName ? $this->$keyName : $this->getKey(); $keyName = $keyName ?: $this->getKeyName(); From 84104d5ed4212fe9c50cb642b9ace83b08287784 Mon Sep 17 00:00:00 2001 From: October CMS Date: Wed, 8 Nov 2023 13:25:29 +1100 Subject: [PATCH 29/74] Deprecate reloadRelations --- src/Auth/Models/User.php | 4 ++-- src/Database/Model.php | 8 +++----- src/Database/Relations/AttachOneOrMany.php | 4 ++-- src/Database/Relations/BelongsTo.php | 2 +- src/Database/Relations/BelongsToMany.php | 4 ++-- src/Database/Relations/HasOneOrMany.php | 4 ++-- src/Database/Relations/MorphOneOrMany.php | 4 ++-- src/Database/Relations/MorphTo.php | 4 ++-- 8 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/Auth/Models/User.php b/src/Auth/Models/User.php index 30b5e3ce1..dbcc8e4c2 100644 --- a/src/Auth/Models/User.php +++ b/src/Auth/Models/User.php @@ -421,7 +421,7 @@ public function addGroup($group) { if (!$this->inGroup($group)) { $this->groups()->attach($group); - $this->reloadRelations('groups'); + $this->unsetRelation('groups'); } return true; @@ -436,7 +436,7 @@ public function removeGroup($group) { if ($this->inGroup($group)) { $this->groups()->detach($group); - $this->reloadRelations('groups'); + $this->unsetRelation('groups'); } return true; diff --git a/src/Database/Model.php b/src/Database/Model.php index 8a8af0a11..8a7382b6b 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -110,17 +110,15 @@ public function reload() } /** - * reloadRelations for this model. - * @param string $relationName - * @return void + * @deprecated use unsetRelation or unsetRelations */ public function reloadRelations($relationName = null) { if (!$relationName) { - $this->setRelations([]); + $this->unsetRelations(); } else { - unset($this->relations[$relationName]); + $this->unsetRelation($relationName); } } diff --git a/src/Database/Relations/AttachOneOrMany.php b/src/Database/Relations/AttachOneOrMany.php index 2cf0c171f..181e5b4d4 100644 --- a/src/Database/Relations/AttachOneOrMany.php +++ b/src/Database/Relations/AttachOneOrMany.php @@ -200,7 +200,7 @@ public function add(Model $model, $sessionKey = null) $this->parent->setRelation($this->relationName, $model); } else { - $this->parent->reloadRelations($this->relationName); + $this->parent->unsetRelation($this->relationName); } /** @@ -280,7 +280,7 @@ public function remove(Model $model, $sessionKey = null) $this->parent->setRelation($this->relationName, null); } else { - $this->parent->reloadRelations($this->relationName); + $this->parent->unsetRelation($this->relationName); } /** diff --git a/src/Database/Relations/BelongsTo.php b/src/Database/Relations/BelongsTo.php index 190809449..905bffe82 100644 --- a/src/Database/Relations/BelongsTo.php +++ b/src/Database/Relations/BelongsTo.php @@ -180,7 +180,7 @@ public function setSimpleValue($value) } else { $this->child->setAttribute($this->getForeignKeyName(), $value); - $this->child->reloadRelations($this->relationName); + $this->child->unsetRelation($this->relationName); } } diff --git a/src/Database/Relations/BelongsToMany.php b/src/Database/Relations/BelongsToMany.php index d9663ac56..b52186eb3 100644 --- a/src/Database/Relations/BelongsToMany.php +++ b/src/Database/Relations/BelongsToMany.php @@ -232,7 +232,7 @@ public function add(Model $model, $sessionKey = null, $pivotData = []) }); } - $this->parent->reloadRelations($this->relationName); + $this->parent->unsetRelation($this->relationName); } else { $this->parent->bindDeferred($this->relationName, $model, $sessionKey, $pivotData); @@ -246,7 +246,7 @@ public function remove(Model $model, $sessionKey = null) { if ($sessionKey === null) { $this->detach($model); - $this->parent->reloadRelations($this->relationName); + $this->parent->unsetRelation($this->relationName); } else { $this->parent->unbindDeferred($this->relationName, $model, $sessionKey); diff --git a/src/Database/Relations/HasOneOrMany.php b/src/Database/Relations/HasOneOrMany.php index 7591f7d19..2f3b9bfaf 100644 --- a/src/Database/Relations/HasOneOrMany.php +++ b/src/Database/Relations/HasOneOrMany.php @@ -96,7 +96,7 @@ public function add(Model $model, $sessionKey = null) $this->parent->setRelation($this->relationName, $model); } else { - $this->parent->reloadRelations($this->relationName); + $this->parent->unsetRelation($this->relationName); } /** @@ -174,7 +174,7 @@ public function remove(Model $model, $sessionKey = null) $this->parent->setRelation($this->relationName, null); } else { - $this->parent->reloadRelations($this->relationName); + $this->parent->unsetRelation($this->relationName); } /** diff --git a/src/Database/Relations/MorphOneOrMany.php b/src/Database/Relations/MorphOneOrMany.php index d9d97815d..0758fd7c7 100644 --- a/src/Database/Relations/MorphOneOrMany.php +++ b/src/Database/Relations/MorphOneOrMany.php @@ -87,7 +87,7 @@ public function add(Model $model, $sessionKey = null) $this->parent->setRelation($this->relationName, $model); } else { - $this->parent->reloadRelations($this->relationName); + $this->parent->unsetRelation($this->relationName); } /** @@ -154,7 +154,7 @@ public function remove(Model $model, $sessionKey = null) $this->parent->setRelation($this->relationName, null); } else { - $this->parent->reloadRelations($this->relationName); + $this->parent->unsetRelation($this->relationName); } /** diff --git a/src/Database/Relations/MorphTo.php b/src/Database/Relations/MorphTo.php index 7835eb0b1..dfdca99e2 100644 --- a/src/Database/Relations/MorphTo.php +++ b/src/Database/Relations/MorphTo.php @@ -146,11 +146,11 @@ public function setSimpleValue($value) [$modelId, $modelClass] = $value; $this->parent->setAttribute($this->foreignKey, $modelId); $this->parent->setAttribute($this->morphType, $modelClass); - $this->parent->reloadRelations($this->relationName); + $this->parent->unsetRelation($this->relationName); } else { $this->parent->setAttribute($this->foreignKey, $value); - $this->parent->reloadRelations($this->relationName); + $this->parent->unsetRelation($this->relationName); } } From 03740c80c26b18545ec0540391b4b16da03325b1 Mon Sep 17 00:00:00 2001 From: October CMS Date: Fri, 10 Nov 2023 14:40:24 +1100 Subject: [PATCH 30/74] Improve string-based rule matching on removal --- src/Database/Traits/Validation.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Database/Traits/Validation.php b/src/Database/Traits/Validation.php index 21137ccac..f436c642f 100644 --- a/src/Database/Traits/Validation.php +++ b/src/Database/Traits/Validation.php @@ -147,6 +147,13 @@ public function removeValidationRule(string $name, $definition) if ($rule === $definition) { unset($rules[$key]); } + elseif ( + is_string($definition) && + is_string($rule) && + str_starts_with($rule, "{$definition}:") + ) { + unset($rules[$key]); + } } $this->rules[$name] = $rules; From 37dd8db2971897043755e77f3d87a801ec128cee Mon Sep 17 00:00:00 2001 From: October CMS Date: Thu, 16 Nov 2023 11:10:12 +1100 Subject: [PATCH 31/74] Adds Str::limitMiddle --- src/Support/Str.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Support/Str.php b/src/Support/Str.php index 5a9643dff..71f70a468 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -116,4 +116,31 @@ public static function getPrecedingSymbols(string $string, string $symbol): int { return strlen($string) - strlen(ltrim($string, $symbol)); } + + /** + * limitMiddle limits the length of a string by removing characters from the middle + * + * @param string $value + * @param int $limit + * @param string $marker + * @return string + */ + public static function limitMiddle($value, $limit = 100, $marker = '...') + { + if (mb_strwidth($value, 'UTF-8') <= $limit) { + return $value; + } + + if ($limit > 3) { + $limit -= 3; + } + + $limitStart = floor($limit / 2); + $limitEnd = $limit - $limitStart; + + $valueStart = rtrim(mb_strimwidth($value, 0, $limitStart, '', 'UTF-8')); + $valueEnd = ltrim(mb_strimwidth($value, $limitEnd * -1, $limitEnd, '', 'UTF-8')); + + return $valueStart . $marker . $valueEnd; + } } From 8b5fb1bb8ce9f77585d4ace2aaa46b556f26e17c Mon Sep 17 00:00:00 2001 From: October CMS Date: Sat, 18 Nov 2023 18:01:18 +1100 Subject: [PATCH 32/74] Improves eager loading of attachments This co-mingles eager loading for attachMany/attachOne relations so multiple common attachment relations are bundled together. This allows a model to have 30+ attach relations without much of a performance impact. --- src/Database/Builder.php | 19 ++++++ .../Concerns/HasEagerLoadAttachRelation.php | 66 +++++++++++++++++++ src/Database/Relations/AttachOneOrMany.php | 12 ++++ 3 files changed, 97 insertions(+) create mode 100644 src/Database/Concerns/HasEagerLoadAttachRelation.php diff --git a/src/Database/Builder.php b/src/Database/Builder.php index 7b330d612..938c9d1ee 100644 --- a/src/Database/Builder.php +++ b/src/Database/Builder.php @@ -3,6 +3,7 @@ use Illuminate\Pagination\Paginator; use Illuminate\Database\Eloquent\Builder as BuilderModel; use October\Rain\Support\Facades\DbDongle; +use Closure; /** * Builder class for queries, extends the Eloquent builder class. @@ -13,6 +14,24 @@ class Builder extends BuilderModel { use \October\Rain\Database\Concerns\HasNicerPagination; + use \October\Rain\Database\Concerns\HasEagerLoadAttachRelation; + + /** + * eagerLoadRelation eagerly load the relationship on a set of models, with support + * for attach relations. + * @param array $models + * @param string $name + * @param \Closure $constraints + * @return array + */ + protected function eagerLoadRelation(array $models, $name, Closure $constraints) + { + if ($result = $this->eagerLoadAttachRelation($models, $name, $constraints)) { + return $result; + } + + return parent::eagerLoadRelation($models, $name, $constraints); + } /** * lists gets an array with the values of a given column. diff --git a/src/Database/Concerns/HasEagerLoadAttachRelation.php b/src/Database/Concerns/HasEagerLoadAttachRelation.php new file mode 100644 index 000000000..4dbaa0391 --- /dev/null +++ b/src/Database/Concerns/HasEagerLoadAttachRelation.php @@ -0,0 +1,66 @@ +getModel()->getRelationType($name); + if (!$relationType || !in_array($relationType, ['attachOne', 'attachMany'])) { + return null; + } + + // Only vanilla attachments are supported, pass complex lookups back to Laravel + $definition = $this->getModel()->getRelationDefinition($name); + if (isset($definition['conditions']) || isset($definition['scope'])) { + return null; + } + + // Opt-out of the combined eager loading logic + if (isset($definition['combineEager']) && $definition['combineEager'] === false) { + return null; + } + + $relation = $this->getRelation($name); + $relatedModel = get_class($relation->getRelated()); + + // Perform a global look up attachment without the 'field' constraint + // to produce a combined subset of all possible attachment relations. + if (!isset($this->eagerLoadAttachResultCache[$relatedModel])) { + $relation->addCommonEagerConstraints($models); + + // Note this takes first constraint only. If it becomes a problem one solution + // could be to compare the md5 of toSql() to ensure uniqueness. The workaround + // for this edge case is to set combineEager => false in the definition. + $constraints($relation); + + $this->eagerLoadAttachResultCache[$relatedModel] = $relation->getEager(); + } + + $results = $this->eagerLoadAttachResultCache[$relatedModel]; + + return $relation->match( + $relation->initRelation($models, $name), + $results->where('field', $name), + $name + ); + } +} diff --git a/src/Database/Relations/AttachOneOrMany.php b/src/Database/Relations/AttachOneOrMany.php index 181e5b4d4..e6f2c71e1 100644 --- a/src/Database/Relations/AttachOneOrMany.php +++ b/src/Database/Relations/AttachOneOrMany.php @@ -105,6 +105,18 @@ public function addEagerConstraints(array $models) $this->query->where('field', $this->relationName); } + /** + * addCommonEagerConstraints adds constraints without the field constraint, used to + * eager load multiple relations of a common type. + * @see \October\Rain\Database\Concerns\HasEagerLoadAttachRelation + * @param array $models + * @return void + */ + public function addCommonEagerConstraints(array $models) + { + parent::addEagerConstraints($models); + } + /** * save the supplied related model */ From 17ce16d17eb4981ff31869bcd974dee24e71685c Mon Sep 17 00:00:00 2001 From: October CMS Date: Mon, 20 Nov 2023 10:44:28 +1100 Subject: [PATCH 33/74] =?UTF-8?q?get|setRelationSimpleValue=20=E2=86=92=20?= =?UTF-8?q?get|setRelationValue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a collision with Laravel method API --- src/Database/Concerns/HasAttributes.php | 2 +- src/Database/Concerns/HasRelationships.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Database/Concerns/HasAttributes.php b/src/Database/Concerns/HasAttributes.php index 7fcb415bb..fd5361b21 100644 --- a/src/Database/Concerns/HasAttributes.php +++ b/src/Database/Concerns/HasAttributes.php @@ -164,7 +164,7 @@ public function setAttribute($key, $value) // Handle direct relation setting if ($this->hasRelation($key) && !$this->hasSetMutator($key)) { - return $this->setRelationValue($key, $value); + return $this->setRelationSimpleValue($key, $value); } /** diff --git a/src/Database/Concerns/HasRelationships.php b/src/Database/Concerns/HasRelationships.php index db9dc3fd1..55fed3094 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -848,17 +848,17 @@ protected function getRelationCaller() } /** - * getRelationValue returns a relation key value(s), not as an object. + * getRelationSimpleValue returns a relation key value(s), not as an object. */ - public function getRelationValue($relationName) + public function getRelationSimpleValue($relationName) { return $this->$relationName()->getSimpleValue(); } /** - * setRelationValue sets a relation value directly from its attribute. + * setRelationSimpleValue sets a relation value directly from its attribute. */ - protected function setRelationValue($relationName, $value) + protected function setRelationSimpleValue($relationName, $value) { $this->$relationName()->setSimpleValue($value); } From c6ae411b853c9872e7e603dfe87a793dc93a9a71 Mon Sep 17 00:00:00 2001 From: October CMS Date: Mon, 20 Nov 2023 18:21:19 +1100 Subject: [PATCH 34/74] Use disk name as simple value for attachments --- src/Database/Attach/File.php | 32 ++++++++++++++++++++-- src/Database/Relations/AttachMany.php | 2 +- src/Database/Relations/AttachOne.php | 8 ++++-- src/Database/Relations/AttachOneOrMany.php | 5 ++-- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/Database/Attach/File.php b/src/Database/Attach/File.php index e28c5a8c5..97695d5d7 100644 --- a/src/Database/Attach/File.php +++ b/src/Database/Attach/File.php @@ -148,6 +148,35 @@ public function fromFile($filePath, $filename = null) return $this; } + /** + * fromDiskName clones an existing file object from its disk name + * @param string $diskName + * @param string $filename + * @return $this + */ + public function fromDiskName($diskName, $filename = null) + { + if ($diskName === null || !$this->hasFile($diskName)) { + return; + } + + $otherFile = $this + ->newQueryWithoutScopes() + ->where('disk_name', $diskName) + ->first() + ; + + if (!$otherFile) { + return; + } + + $this->file_name = empty($filename) ? $otherFile->file_name : $filename; + $this->file_size = $otherFile->file_size; + $this->content_type = $otherFile->content_type; + $this->disk_name = $otherFile->disk_name; + return $this; + } + /** * fromData creates a file object from raw data * @param string $data @@ -517,9 +546,8 @@ public function beforeSave() if ($this->data instanceof UploadedFile) { $this->fromPost($this->data); } - // @deprecated see AttachOneOrMany::isValidFileData else { - $this->fromFile($this->data); + $this->fromDiskName($this->data); } $this->data = null; diff --git a/src/Database/Relations/AttachMany.php b/src/Database/Relations/AttachMany.php index 4e4b0bbdc..d2e80308b 100644 --- a/src/Database/Relations/AttachMany.php +++ b/src/Database/Relations/AttachMany.php @@ -85,7 +85,7 @@ public function getSimpleValue() if ($files) { $value = []; foreach ($files as $file) { - $value[] = $file->getPath(); + $value[] = $file->disk_name; } } diff --git a/src/Database/Relations/AttachOne.php b/src/Database/Relations/AttachOne.php index eba55c974..40110ea0e 100644 --- a/src/Database/Relations/AttachOne.php +++ b/src/Database/Relations/AttachOne.php @@ -71,10 +71,14 @@ public function getSimpleValue() $relationName = $this->relationName; if ($this->parent->relationLoaded($relationName)) { - $value = $this->parent->getRelation($relationName); + $file = $this->parent->getRelation($relationName); } else { - $value = $this->getResults(); + $file = $this->getResults(); + } + + if ($file) { + $value = $file->disk_name; } return $value; diff --git a/src/Database/Relations/AttachOneOrMany.php b/src/Database/Relations/AttachOneOrMany.php index e6f2c71e1..59ef5c94d 100644 --- a/src/Database/Relations/AttachOneOrMany.php +++ b/src/Database/Relations/AttachOneOrMany.php @@ -353,12 +353,13 @@ protected function ensureAttachOneIsSingular($sessionKey = null) */ protected function isValidFileData($value) { + // Newly uploaded file if ($value instanceof UploadedFile) { return true; } - // @deprecated this method should be replaced by an instanceof UploadedFile check - if (is_string($value) && file_exists($value)) { + // Disk name + if (is_string($value)) { return true; } From 81f89f10a9d17479082277785f87471cd726148b Mon Sep 17 00:00:00 2001 From: October CMS Date: Mon, 20 Nov 2023 18:49:05 +1100 Subject: [PATCH 35/74] Use key name as simple value instead This should transfer the object like any other hasMany/hasOne --- src/Database/Attach/File.php | 32 ---------------------- src/Database/Relations/AttachMany.php | 9 ++++-- src/Database/Relations/AttachOne.php | 5 ++-- src/Database/Relations/AttachOneOrMany.php | 18 ------------ 4 files changed, 9 insertions(+), 55 deletions(-) diff --git a/src/Database/Attach/File.php b/src/Database/Attach/File.php index 97695d5d7..8efe895d2 100644 --- a/src/Database/Attach/File.php +++ b/src/Database/Attach/File.php @@ -148,35 +148,6 @@ public function fromFile($filePath, $filename = null) return $this; } - /** - * fromDiskName clones an existing file object from its disk name - * @param string $diskName - * @param string $filename - * @return $this - */ - public function fromDiskName($diskName, $filename = null) - { - if ($diskName === null || !$this->hasFile($diskName)) { - return; - } - - $otherFile = $this - ->newQueryWithoutScopes() - ->where('disk_name', $diskName) - ->first() - ; - - if (!$otherFile) { - return; - } - - $this->file_name = empty($filename) ? $otherFile->file_name : $filename; - $this->file_size = $otherFile->file_size; - $this->content_type = $otherFile->content_type; - $this->disk_name = $otherFile->disk_name; - return $this; - } - /** * fromData creates a file object from raw data * @param string $data @@ -546,9 +517,6 @@ public function beforeSave() if ($this->data instanceof UploadedFile) { $this->fromPost($this->data); } - else { - $this->fromDiskName($this->data); - } $this->data = null; } diff --git a/src/Database/Relations/AttachMany.php b/src/Database/Relations/AttachMany.php index d2e80308b..b121c07fa 100644 --- a/src/Database/Relations/AttachMany.php +++ b/src/Database/Relations/AttachMany.php @@ -1,8 +1,11 @@ isValidFileData($value)) { + if ($value instanceof UploadedFile) { $this->parent->bindEventOnce('model.afterSave', function () use ($value) { $this->create(['data' => $value]); }); @@ -47,7 +50,7 @@ public function setSimpleValue($value) elseif (is_array($value)) { $files = []; foreach ($value as $_value) { - if ($this->isValidFileData($_value)) { + if ($_value instanceof UploadedFile) { $files[] = $_value; } } @@ -85,7 +88,7 @@ public function getSimpleValue() if ($files) { $value = []; foreach ($files as $file) { - $value[] = $file->disk_name; + $value[] = $file->getKey(); } } diff --git a/src/Database/Relations/AttachOne.php b/src/Database/Relations/AttachOne.php index 40110ea0e..c6e3c9523 100644 --- a/src/Database/Relations/AttachOne.php +++ b/src/Database/Relations/AttachOne.php @@ -3,6 +3,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\MorphOne as MorphOneBase; +use Symfony\Component\HttpFoundation\File\UploadedFile; use October\Rain\Database\Attach\File as FileModel; /** @@ -43,7 +44,7 @@ public function setSimpleValue($value) } // Newly uploaded file - if ($this->isValidFileData($value)) { + if ($value instanceof UploadedFile) { $this->parent->bindEventOnce('model.afterSave', function () use ($value) { $file = $this->create(['data' => $value]); $this->parent->setRelation($this->relationName, $file); @@ -78,7 +79,7 @@ public function getSimpleValue() } if ($file) { - $value = $file->disk_name; + $value = $file->getKey(); } return $value; diff --git a/src/Database/Relations/AttachOneOrMany.php b/src/Database/Relations/AttachOneOrMany.php index 59ef5c94d..6db6073fe 100644 --- a/src/Database/Relations/AttachOneOrMany.php +++ b/src/Database/Relations/AttachOneOrMany.php @@ -348,24 +348,6 @@ protected function ensureAttachOneIsSingular($sessionKey = null) } } - /** - * isValidFileData returns true if the specified value can be used as the data attribute - */ - protected function isValidFileData($value) - { - // Newly uploaded file - if ($value instanceof UploadedFile) { - return true; - } - - // Disk name - if (is_string($value)) { - return true; - } - - return false; - } - /** * @deprecated this method is removed in October CMS v4 */ From 5891497f627ef101e72ca0f714aee9380875c737 Mon Sep 17 00:00:00 2001 From: October CMS Date: Mon, 20 Nov 2023 18:53:16 +1100 Subject: [PATCH 36/74] Rem deprecated methods --- src/Database/Relations/AttachMany.php | 38 --------------------------- src/Database/Relations/AttachOne.php | 33 ----------------------- 2 files changed, 71 deletions(-) diff --git a/src/Database/Relations/AttachMany.php b/src/Database/Relations/AttachMany.php index b121c07fa..f47e622f5 100644 --- a/src/Database/Relations/AttachMany.php +++ b/src/Database/Relations/AttachMany.php @@ -94,42 +94,4 @@ public function getSimpleValue() return $value; } - - /** - * @deprecated this method is removed in October CMS v4 - */ - public function getValidationValue() - { - if ($value = $this->getSimpleValueInternal()) { - $files = []; - foreach ($value as $file) { - $files[] = $this->makeValidationFile($file); - } - - return $files; - } - - return null; - } - - /** - * @deprecated this method is removed in October CMS v4 - */ - protected function getSimpleValueInternal() - { - $value = null; - - $files = ($sessionKey = $this->parent->sessionKey) - ? $this->withDeferred($sessionKey)->get() - : $this->parent->{$this->relationName}; - - if ($files) { - $value = []; - $files->each(function ($file) use (&$value) { - $value[] = $file; - }); - } - - return $value; - } } diff --git a/src/Database/Relations/AttachOne.php b/src/Database/Relations/AttachOne.php index c6e3c9523..cc4a9f2c3 100644 --- a/src/Database/Relations/AttachOne.php +++ b/src/Database/Relations/AttachOne.php @@ -56,9 +56,6 @@ public function setSimpleValue($value) $this->add($value); }); } - - // The relation is set here to satisfy `getValidationValue` - $this->parent->setRelation($this->relationName, $value); } /** @@ -84,34 +81,4 @@ public function getSimpleValue() return $value; } - - /** - * @deprecated this method is removed in October CMS v4 - */ - public function getValidationValue() - { - if ($value = $this->getSimpleValueInternal()) { - return $this->makeValidationFile($value); - } - - return null; - } - - /** - * @deprecated this method is removed in October CMS v4 - */ - protected function getSimpleValueInternal() - { - $value = null; - - $file = ($sessionKey = $this->parent->sessionKey) - ? $this->withDeferred($sessionKey)->first() - : $this->parent->{$this->relationName}; - - if ($file) { - $value = $file; - } - - return $value; - } } From 0d9df4900a4a0a606559d5d77a61a5cd89d09b04 Mon Sep 17 00:00:00 2001 From: October CMS Date: Mon, 20 Nov 2023 19:07:15 +1100 Subject: [PATCH 37/74] Refactor attachment set simple value to support more scenarios --- src/Database/Relations/AttachMany.php | 49 +++++++++++++++++++++++---- src/Database/Relations/AttachOne.php | 8 +++++ 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/Database/Relations/AttachMany.php b/src/Database/Relations/AttachMany.php index f47e622f5..c7cdbd1f9 100644 --- a/src/Database/Relations/AttachMany.php +++ b/src/Database/Relations/AttachMany.php @@ -41,29 +41,64 @@ public function __construct(Builder $query, Model $parent, $type, $id, $isPublic */ public function setSimpleValue($value) { - // Newly uploaded file(s) + // Append a single newly uploaded file(s) if ($value instanceof UploadedFile) { $this->parent->bindEventOnce('model.afterSave', function () use ($value) { $this->create(['data' => $value]); }); + return; } - elseif (is_array($value)) { - $files = []; + + // Append existing File model + if ($value instanceof FileModel) { + $this->parent->bindEventOnce('model.afterSave', function () use ($value) { + $this->add($value); + }); + return; + } + + // Process multiple values + $files = $models = $keys = []; + if (is_array($value)) { foreach ($value as $_value) { if ($_value instanceof UploadedFile) { $files[] = $_value; } + elseif ($_value instanceof FileModel) { + $models[] = $_value; + } + elseif (is_numeric($_value)){ + $keys[] = $_value; + } } + } + + if ($files) { $this->parent->bindEventOnce('model.afterSave', function () use ($files) { foreach ($files as $file) { $this->create(['data' => $file]); } }); } - // Existing File model - elseif ($value instanceof FileModel) { - $this->parent->bindEventOnce('model.afterSave', function () use ($value) { - $this->add($value); + + if ($keys) { + $this->parent->bindEventOnce('model.afterSave', function () use ($keys) { + $models = $this->getRelated() + ->whereIn($this->getRelatedKeyName(), (array) $keys) + ->get() + ; + + foreach ($models as $model) { + $this->add($model); + } + }); + } + + if ($models) { + $this->parent->bindEventOnce('model.afterSave', function () use ($models) { + foreach ($models as $model) { + $this->add($model); + } }); } } diff --git a/src/Database/Relations/AttachOne.php b/src/Database/Relations/AttachOne.php index cc4a9f2c3..31eb400b1 100644 --- a/src/Database/Relations/AttachOne.php +++ b/src/Database/Relations/AttachOne.php @@ -56,6 +56,14 @@ public function setSimpleValue($value) $this->add($value); }); } + // Model key + elseif (is_numeric($value)) { + $this->parent->bindEventOnce('model.afterSave', function () use ($value) { + if ($model = $this->getRelated()->find($value)) { + $this->add($model); + } + }); + } } /** From 813a587b0bd6f28dd70b77164bde7c1c119e1374 Mon Sep 17 00:00:00 2001 From: October CMS Date: Wed, 22 Nov 2023 14:51:00 +1100 Subject: [PATCH 38/74] Adds nesting to makeRelation --- src/Database/Concerns/HasRelationships.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Database/Concerns/HasRelationships.php b/src/Database/Concerns/HasRelationships.php index 55fed3094..60f132402 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -254,12 +254,24 @@ public function isRelationTypeSingular($name): bool } /** - * makeRelation returns a relation class object - * @param string $name Relation name - * @return object + * makeRelation returns a relation class object, supporting nested relations with + * dot notation + * @param string $name + * @return \Model|null */ public function makeRelation($name) { + if (str_contains($name, '.')) { + $model = $this; + $parts = explode('.', $name); + while ($relationName = array_shift($parts)) { + if (!$model = $model->makeRelation($relationName)) { + return null; + } + } + return $model; + } + $relation = $this->getRelationDefinition($name); $relationType = $this->getRelationType($name); From 72c27a7445c564d095098f22aeafeeb9be8421a7 Mon Sep 17 00:00:00 2001 From: October CMS Date: Wed, 22 Nov 2023 14:53:10 +1100 Subject: [PATCH 39/74] Adds nameToDot to html helper --- src/Html/Helper.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Html/Helper.php b/src/Html/Helper.php index 5f45ca5b6..d1912b5dc 100644 --- a/src/Html/Helper.php +++ b/src/Html/Helper.php @@ -24,7 +24,7 @@ public static function nameToId($string) * nameToArray converts a HTML named array string to a PHP array. Empty values are removed. * HTML: user[location][city] * PHP: ['user', 'location', 'city'] - * @param $string String to process + * @param $string * @return array */ public static function nameToArray($string) @@ -51,6 +51,18 @@ public static function nameToArray($string) return $result; } + /** + * nameToDot converts a HTML named array string to a dot notated string. + * HTML: user[location][city] + * Dot: user.location.city + * @param $string + * @return string + */ + public static function nameToDot($string) + { + return implode('.', static::nameToArray($string)); + } + /** * reduceNameHierarchy reduces the field name hierarchy depth by $level levels. * country[city][0][street][0] turns into country[city][0] when reduced by 1 level; From cfcee0dfbecf618a468dfb437e10b04af8926d8a Mon Sep 17 00:00:00 2001 From: October CMS Date: Wed, 22 Nov 2023 14:55:35 +1100 Subject: [PATCH 40/74] Refit nameToDot --- src/Support/helpers.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Support/helpers.php b/src/Support/helpers.php index d3fc2f012..16dc44232 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -26,7 +26,7 @@ function input($name = null, $default = null) // Array field name, eg: field[key][key2][key3] if (class_exists('October\Rain\Html\Helper')) { - $name = implode('.', October\Rain\Html\Helper::nameToArray($name)); + $name = October\Rain\Html\Helper::nameToDot($name); } return array_get(Request::all(), $name, $default); @@ -49,7 +49,7 @@ function post($name = null, $default = null) // Array field name, eg: field[key][key2][key3] if (class_exists('October\Rain\Html\Helper')) { - $name = implode('.', October\Rain\Html\Helper::nameToArray($name)); + $name = October\Rain\Html\Helper::nameToDot($name); } return array_get(Request::post(), $name, $default); @@ -68,7 +68,7 @@ function get($name = null, $default = null) // Array field name, eg: field[key][key2][key3] if (class_exists('October\Rain\Html\Helper')) { - $name = implode('.', October\Rain\Html\Helper::nameToArray($name)); + $name = October\Rain\Html\Helper::nameToDot($name); } return array_get(Request::query(), $name, $default); @@ -87,7 +87,7 @@ function files($name = null, $default = null) // Array field name, eg: field[key][key2][key3] if (class_exists('October\Rain\Html\Helper')) { - $name = implode('.', October\Rain\Html\Helper::nameToArray($name)); + $name = October\Rain\Html\Helper::nameToDot($name); } return array_get(Request::allFiles(), $name, $default); From 89c573a6ebd19d91e9678b976963f34df4f38179 Mon Sep 17 00:00:00 2001 From: October CMS Date: Sun, 26 Nov 2023 11:53:13 +1100 Subject: [PATCH 41/74] Adds user footprint trait --- src/Auth/Concerns/HasProviderProxy.php | 26 ++++++++ src/Auth/Manager.php | 1 + src/Database/Traits/UserFootprints.php | 82 ++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 src/Auth/Concerns/HasProviderProxy.php create mode 100644 src/Database/Traits/UserFootprints.php diff --git a/src/Auth/Concerns/HasProviderProxy.php b/src/Auth/Concerns/HasProviderProxy.php new file mode 100644 index 000000000..81b543faf --- /dev/null +++ b/src/Auth/Concerns/HasProviderProxy.php @@ -0,0 +1,26 @@ +userModel; + } +} diff --git a/src/Auth/Manager.php b/src/Auth/Manager.php index 20312cdc8..c593a7b49 100644 --- a/src/Auth/Manager.php +++ b/src/Auth/Manager.php @@ -17,6 +17,7 @@ class Manager implements StatefulGuard use \October\Rain\Auth\Concerns\HasThrottle; use \October\Rain\Auth\Concerns\HasImpersonation; use \October\Rain\Auth\Concerns\HasStatefulGuard; + use \October\Rain\Auth\Concerns\HasProviderProxy; use \October\Rain\Auth\Concerns\HasGuard; /** diff --git a/src/Database/Traits/UserFootprints.php b/src/Database/Traits/UserFootprints.php new file mode 100644 index 000000000..5ad82aa27 --- /dev/null +++ b/src/Database/Traits/UserFootprints.php @@ -0,0 +1,82 @@ +bindEvent('model.saveInternal', function () { + $this->updateUserFootprints(); + }); + + $userModel = $this->getUserFootprintAuth()->getProvider()->getModel(); + + $this->belongsTo['updated_user'] = [ + $userModel, + 'replicate' => false + ]; + + $this->belongsTo['created_user'] = [ + $userModel, + 'replicate' => false + ]; + } + + /** + * updateUserFootprints + */ + public function updateUserFootprints() + { + $userId = $this->getUserFootprintAuth()->id(); + if (!$userId) { + return; + } + + $updatedColumn = $this->getUpdatedUserIdColumn(); + if ($updatedColumn !== null && !$this->isDirty($updatedColumn)) { + $this->{$updatedColumn} = $userId; + } + + $createdColumn = $this->getCreatedUserIdColumn(); + if (!$this->exists && $createdColumn !== null && $this->isDirty($createdColumn)) { + $this->{$createdColumn} = $userId; + } + } + + /** + * getCreatedUserIdColumn gets the name of the "created user id" column. + * @return string + */ + public function getCreatedUserIdColumn() + { + return defined('static::CREATED_USER_ID') ? static::CREATED_USER_ID : 'created_user_id'; + } + + /** + * getCreatedUserIdColumn gets the name of the "updated user id" column. + * @return string + */ + public function getUpdatedUserIdColumn() + { + return defined('static::UPDATED_USER_ID') ? static::UPDATED_USER_ID : 'updated_user_id'; + } + + /** + * getUserFootprintAuth + */ + protected function getUserFootprintAuth() + { + return App::make('backend.auth'); + } +} From e084ce8359ec04d364619923bce01014577ff173 Mon Sep 17 00:00:00 2001 From: October CMS Date: Wed, 29 Nov 2023 14:38:52 +1100 Subject: [PATCH 42/74] Adds sendTo hint --- src/Mail/Mailer.php | 2 -- src/Support/Facades/Mail.php | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mail/Mailer.php b/src/Mail/Mailer.php index 19c3908bb..e958498b3 100644 --- a/src/Mail/Mailer.php +++ b/src/Mail/Mailer.php @@ -187,9 +187,7 @@ public function sendTo($recipients, $view, array $data = [], $callback = null, $ $recipients = $this->processRecipients($recipients); return $this->{$method}($view, $data, function ($message) use ($recipients, $callback, $bcc) { - $method = $bcc === true ? 'bcc' : 'to'; - foreach ($recipients as $address => $name) { $message->{$method}($address, $name); } diff --git a/src/Support/Facades/Mail.php b/src/Support/Facades/Mail.php index b8d716caf..42ae5ec3b 100644 --- a/src/Support/Facades/Mail.php +++ b/src/Support/Facades/Mail.php @@ -6,6 +6,8 @@ /** * Mail * + * @method static void sendTo(mixed $recipients, string $view, array $data = [], $callback = null, $options = []) + * * @see \October\Rain\Mails\Dispatcher */ class Mail extends MailBase From 66ea962f0716351e6b68c05ac7d1b7acee3966b4 Mon Sep 17 00:00:00 2001 From: October CMS Date: Wed, 29 Nov 2023 14:39:02 +1100 Subject: [PATCH 43/74] Minor bugfix --- src/Database/Traits/UserFootprints.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Traits/UserFootprints.php b/src/Database/Traits/UserFootprints.php index 5ad82aa27..9053ffeb2 100644 --- a/src/Database/Traits/UserFootprints.php +++ b/src/Database/Traits/UserFootprints.php @@ -49,7 +49,7 @@ public function updateUserFootprints() } $createdColumn = $this->getCreatedUserIdColumn(); - if (!$this->exists && $createdColumn !== null && $this->isDirty($createdColumn)) { + if (!$this->exists && $createdColumn !== null && !$this->isDirty($createdColumn)) { $this->{$createdColumn} = $userId; } } From babd95ce72c053490aafcfa9457dfef9ecef0130 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Fri, 1 Dec 2023 14:11:42 +1100 Subject: [PATCH 44/74] Minor --- src/Filesystem/Filesystem.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Filesystem/Filesystem.php b/src/Filesystem/Filesystem.php index 1be4a6f84..ca774757e 100644 --- a/src/Filesystem/Filesystem.php +++ b/src/Filesystem/Filesystem.php @@ -211,7 +211,8 @@ public function normalizePath($path) } /** - * nicePath returns a nice path that is suitable for sharing. + * nicePath removes the base path from a local path and returns a relatively nice + * path that is suitable and safe for sharing. * @param string $path * @return string */ @@ -307,9 +308,7 @@ public function makeDirectory($path, $mode = 0755, $recursive = false, $force = $mode = $mask; } - /* - * Find the green leaves - */ + // Find the green leaves if ($recursive && $mask) { $chmodPath = $path; while (true) { @@ -327,14 +326,10 @@ public function makeDirectory($path, $mode = 0755, $recursive = false, $force = $chmodPath = $path; } - /* - * Make the directory - */ + // Make the directory $result = parent::makeDirectory($path, $mode, $recursive, $force); - /* - * Apply the permissions - */ + // Apply the permissions if ($mask) { $this->chmod($chmodPath, $mask); From cc1c001666be6c60c4180d36f351dd60ba37c8f7 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Fri, 1 Dec 2023 20:38:22 +1100 Subject: [PATCH 45/74] str_contains in php8 --- src/Support/helpers.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 16dc44232..f384a3c1e 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -668,19 +668,6 @@ function str_before($subject, $search) } } -if (!function_exists('str_contains')) { - /** - * str_contains determines if a given string contains a given substring - * @param string $haystack - * @param string|array $needles - * @return bool - */ - function str_contains($haystack, $needles) - { - return Str::contains($haystack, $needles); - } -} - if (!function_exists('str_finish')) { /** * str_finish caps a string with a single instance of a given value From c0d8ee73d5a470cfe0407f9820004a10992b8a07 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sun, 3 Dec 2023 13:42:23 +1100 Subject: [PATCH 46/74] Minor --- src/Html/UrlServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Html/UrlServiceProvider.php b/src/Html/UrlServiceProvider.php index dea31adfd..9bb4e5bd6 100644 --- a/src/Html/UrlServiceProvider.php +++ b/src/Html/UrlServiceProvider.php @@ -65,7 +65,7 @@ public function registerRelativeHelper() return (new \October\Rain\Html\UrlMixin($provider))->toRelative(...$args); } - return $provider->url(...$args); + return $provider->to(...$args); }); $provider->macro('toSigned', function(...$args) use ($provider) { From 5dceb1b17faf9096402377dd0cc700b2744922a4 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Tue, 5 Dec 2023 17:29:09 +1100 Subject: [PATCH 47/74] Adds interface for twig accessor --- contracts/Twig/ForwardsAttributes.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 contracts/Twig/ForwardsAttributes.php diff --git a/contracts/Twig/ForwardsAttributes.php b/contracts/Twig/ForwardsAttributes.php new file mode 100644 index 000000000..15e0ca767 --- /dev/null +++ b/contracts/Twig/ForwardsAttributes.php @@ -0,0 +1,16 @@ + Date: Thu, 4 Jan 2024 16:38:26 +1100 Subject: [PATCH 48/74] Remove unused ForwardsAttributes --- contracts/Twig/ForwardsAttributes.php | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 contracts/Twig/ForwardsAttributes.php diff --git a/contracts/Twig/ForwardsAttributes.php b/contracts/Twig/ForwardsAttributes.php deleted file mode 100644 index 15e0ca767..000000000 --- a/contracts/Twig/ForwardsAttributes.php +++ /dev/null @@ -1,16 +0,0 @@ - Date: Thu, 4 Jan 2024 16:40:20 +1100 Subject: [PATCH 49/74] Bump wikimedia/less --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4109e4119..72d888b8c 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "doctrine/dbal": "^2.13.3|^3.1.4", "jenssegers/agent": "^2.6", "linkorb/jsmin-php": "~1.0", - "wikimedia/less.php": "~3.0", + "wikimedia/less.php": "~4.1", "pragmarx/google2fa": "^8.0", "bacon/bacon-qr-code": "^2.0", "scssphp/scssphp": "~1.0", From f4399e03b9e6477c6f275a5a37a46ab904e1869f Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 24 Jan 2024 13:56:42 +1100 Subject: [PATCH 50/74] Move these requirements to plugin --- composer.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/composer.json b/composer.json index 72d888b8c..183dcf866 100644 --- a/composer.json +++ b/composer.json @@ -17,11 +17,8 @@ "php": "^8.0.2", "composer/composer": "^2.0.0", "doctrine/dbal": "^2.13.3|^3.1.4", - "jenssegers/agent": "^2.6", "linkorb/jsmin-php": "~1.0", "wikimedia/less.php": "~4.1", - "pragmarx/google2fa": "^8.0", - "bacon/bacon-qr-code": "^2.0", "scssphp/scssphp": "~1.0", "symfony/yaml": "^6.0", "twig/twig": "~3.0", From d863b9a18118600c725f1abb921fb055eb27cebe Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 24 Jan 2024 14:26:13 +1100 Subject: [PATCH 51/74] Make some facades generic --- helpers/Auth.php | 2 +- helpers/Currency.php | 4 ++-- src/Support/Facades/Currency.php | 23 +++++++++++++++++++++++ src/Support/Facades/Site.php | 2 +- 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/Support/Facades/Currency.php diff --git a/helpers/Auth.php b/helpers/Auth.php index 13dfe11ce..077e9f178 100644 --- a/helpers/Auth.php +++ b/helpers/Auth.php @@ -3,6 +3,6 @@ /** * Auth * - * @see \User\Classes\AuthManager + * @see \Responsiv\User\Classes\AuthManager */ class Auth extends October\Rain\Support\Facades\Auth {} diff --git a/helpers/Currency.php b/helpers/Currency.php index fb95f7632..30da555aa 100644 --- a/helpers/Currency.php +++ b/helpers/Currency.php @@ -3,6 +3,6 @@ /** * Currency * - * @see \Shop\Classes\CurrencyManager + * @see \Responsiv\Shop\Classes\CurrencyManager */ -class Currency extends Shop\Facades\Currency {} +class Currency extends October\Rain\Support\Facades\Currency {} diff --git a/src/Support/Facades/Currency.php b/src/Support/Facades/Currency.php new file mode 100644 index 000000000..3996a87a8 --- /dev/null +++ b/src/Support/Facades/Currency.php @@ -0,0 +1,23 @@ + Date: Mon, 29 Jan 2024 12:06:29 +1100 Subject: [PATCH 52/74] Adds number API found in docs --- src/Html/FormBuilder.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Html/FormBuilder.php b/src/Html/FormBuilder.php index 3831d9467..cab7fa687 100644 --- a/src/Html/FormBuilder.php +++ b/src/Html/FormBuilder.php @@ -351,6 +351,18 @@ public function email($name, $value = null, $options = []) return $this->input('email', $name, $value, $options); } + /** + * number input field. + * @param string $name + * @param string $value + * @param array $options + * @return string + */ + public function number($name, $value = null, $options = []) + { + return $this->input('number', $name, $value, $options); + } + /** * url input field. * @param string $name From d00c6a9690d1f01e18f8570286050705ea1f831a Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Mon, 29 Jan 2024 21:00:09 +1100 Subject: [PATCH 53/74] Adds createFromFile since data no longer accepts a local path --- src/Database/Relations/AttachMany.php | 2 -- src/Database/Relations/AttachOne.php | 3 +++ src/Database/Relations/AttachOneOrMany.php | 26 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Database/Relations/AttachMany.php b/src/Database/Relations/AttachMany.php index c7cdbd1f9..e57465d1b 100644 --- a/src/Database/Relations/AttachMany.php +++ b/src/Database/Relations/AttachMany.php @@ -1,10 +1,8 @@ parent->setRelation($this->relationName, $value); } /** diff --git a/src/Database/Relations/AttachOneOrMany.php b/src/Database/Relations/AttachOneOrMany.php index 6db6073fe..d3d80a883 100644 --- a/src/Database/Relations/AttachOneOrMany.php +++ b/src/Database/Relations/AttachOneOrMany.php @@ -162,6 +162,32 @@ public function create(array $attributes = [], $sessionKey = null) return $model; } + /** + * createFromFile + */ + public function createFromFile(string $filePath, array $attributes = [], $sessionKey = null) + { + if (!array_key_exists('is_public', $attributes)) { + $attributes = array_merge(['is_public' => $this->isPublic()], $attributes); + } + + $attributes['field'] = $this->relationName; + + if ($sessionKey === null) { + $this->ensureAttachOneIsSingular(); + } + + $model = parent::make($attributes); + $model->fromFile($filePath); + $model->save(); + + if ($sessionKey !== null) { + $this->add($model, $sessionKey); + } + + return $model; + } + /** * add a model to this relationship type */ From 0f640e67f443e39b813c08eb76eaf3c6f09f32d0 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Tue, 30 Jan 2024 13:00:05 +1100 Subject: [PATCH 54/74] Exception handling --- src/Database/Models/DeferredBinding.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database/Models/DeferredBinding.php b/src/Database/Models/DeferredBinding.php index b00c5175f..30cedbfa8 100644 --- a/src/Database/Models/DeferredBinding.php +++ b/src/Database/Models/DeferredBinding.php @@ -206,7 +206,7 @@ protected function deleteSlaveRecord() // Only delete it if the relationship is null $foreignKey = array_get($options, 'key', $masterObject->getForeignKey()); - if (!$relatedObj->$foreignKey) { + if ($foreignKey && !$relatedObj->$foreignKey) { $relatedObj->delete(); } } From c5c503872e65be76854c925d086c2a49e21abf9d Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Fri, 9 Feb 2024 11:54:59 +1100 Subject: [PATCH 55/74] Adds helpful doc --- src/Database/Traits/BaseIdentifier.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Database/Traits/BaseIdentifier.php b/src/Database/Traits/BaseIdentifier.php index 1e0a8c1cc..44be7ccb7 100644 --- a/src/Database/Traits/BaseIdentifier.php +++ b/src/Database/Traits/BaseIdentifier.php @@ -5,6 +5,10 @@ * lookup key that is immune to enumeration attacks. The model is assumed to have * the attribute: baseid. * + * Add this to your database table with: + * + * $table->string('baseid')->nullable()->index(); + * * @package october\database * @author Alexey Bobkov, Samuel Georges */ From 3f737c53d83e8a7bf87ce07dc47e8d93fa0596c3 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Fri, 9 Feb 2024 17:55:37 +1100 Subject: [PATCH 56/74] Update stubs to latest spec --- src/Scaffold/Console/controller/create.stub | 35 +++++++++++------ src/Scaffold/Console/controller/update.stub | 43 ++++++++++++++------- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/src/Scaffold/Console/controller/create.stub b/src/Scaffold/Console/controller/create.stub index e00acf108..61609668b 100644 --- a/src/Scaffold/Console/controller/create.stub +++ b/src/Scaffold/Console/controller/create.stub @@ -7,33 +7,38 @@ fatalError): ?> - 'layout']) ?> + 'd-flex flex-column h-100']) ?> -
+
formRender() ?>
-
+
- + + + +
@@ -42,7 +47,15 @@ -

fatalError) ?>

-

+

+ fatalError) ?> +

+

+ + + +

diff --git a/src/Scaffold/Console/controller/update.stub b/src/Scaffold/Console/controller/update.stub index f6609c4ea..dee6c287b 100644 --- a/src/Scaffold/Console/controller/update.stub +++ b/src/Scaffold/Console/controller/update.stub @@ -7,42 +7,47 @@ fatalError): ?> - 'layout']) ?> + 'd-flex flex-column h-100']) ?> -
+
formRender() ?>
-
+
- + + + +
@@ -51,7 +56,15 @@ -

fatalError) ?>

-

+

+ fatalError) ?> +

+

+ + + +

From e014a0d5727ab696ea2a965df64108ccdecb004f Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Fri, 9 Feb 2024 18:13:15 +1100 Subject: [PATCH 57/74] Update list toolbar to latest spec --- .../Console/controller/_list_toolbar.stub | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Scaffold/Console/controller/_list_toolbar.stub b/src/Scaffold/Console/controller/_list_toolbar.stub index b89b55f87..2092a00e2 100644 --- a/src/Scaffold/Console/controller/_list_toolbar.stub +++ b/src/Scaffold/Console/controller/_list_toolbar.stub @@ -1,17 +1,23 @@ -
+
- '{{title_singular_name}}'])) ?> + class="btn btn-primary"> + + '{{title_singular_name}}']) ?> + +
+
From 2da0c0fb0c6e6faaee73c5e06fcffce48fddad9d Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Mon, 12 Feb 2024 13:32:11 +1100 Subject: [PATCH 58/74] Doc blocks --- src/Support/Facades/Site.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Support/Facades/Site.php b/src/Support/Facades/Site.php index 317e3cbf3..adc0dda5f 100644 --- a/src/Support/Facades/Site.php +++ b/src/Support/Facades/Site.php @@ -19,6 +19,7 @@ * @method static bool hasGlobalContext() * @method static void withGlobalContext(callable $callback) * @method static void withContext($siteId, callable $callback) + * @method static hasFeature(string $name): bool * * @see \System\Classes\SiteManager */ From d915d7296b48f439ceda6fd6dfc078fac305dc1a Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Mon, 12 Feb 2024 13:32:40 +1100 Subject: [PATCH 59/74] Syntax --- src/Support/Facades/Site.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/Facades/Site.php b/src/Support/Facades/Site.php index adc0dda5f..9ccbd6c42 100644 --- a/src/Support/Facades/Site.php +++ b/src/Support/Facades/Site.php @@ -19,7 +19,7 @@ * @method static bool hasGlobalContext() * @method static void withGlobalContext(callable $callback) * @method static void withContext($siteId, callable $callback) - * @method static hasFeature(string $name): bool + * @method static bool hasFeature(string $name) * * @see \System\Classes\SiteManager */ From 87ff998b398555daa860665b51c8472b4a50800c Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 14 Feb 2024 13:05:18 +1100 Subject: [PATCH 60/74] Move replication logic to db.replicator This gives more room for logic and reduces the memory footprint --- src/Database/Concerns/HasReplication.php | 91 +------------- src/Database/DatabaseServiceProvider.php | 4 +- src/Database/Replicator.php | 153 +++++++++++++++++++++++ 3 files changed, 161 insertions(+), 87 deletions(-) create mode 100644 src/Database/Replicator.php diff --git a/src/Database/Concerns/HasReplication.php b/src/Database/Concerns/HasReplication.php index b9c6422c2..80dbdc4bd 100644 --- a/src/Database/Concerns/HasReplication.php +++ b/src/Database/Concerns/HasReplication.php @@ -1,9 +1,6 @@ replicateRelationsInternal($except); + return App::makeWith('db.replicator', ['model' => $this])->replicate($except); } /** @@ -32,98 +29,20 @@ public function replicateWithRelations(array $except = null) */ public function duplicateWithRelations(array $except = null) { - return $this->replicateRelationsInternal($except, ['isDuplicate' => true]); + return App::makeWith('db.replicator', ['model' => $this])->duplicate($except); } /** - * replicateRelationsInternal + * newReplicationInstance returns a new instance used by the replicator */ - protected function replicateRelationsInternal(array $except = null, array $options = []) + public function newReplicationInstance($attributes) { - extract(array_merge([ - 'isDuplicate' => false - ], $options)); - - $defaults = [ - $this->getKeyName(), - $this->getCreatedAtColumn(), - $this->getUpdatedAtColumn(), - ]; - - $isMultisite = $this->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class); - if ($isMultisite) { - $defaults[] = 'site_root_id'; - } - - $attributes = Arr::except( - $this->attributes, $except ? array_unique(array_merge($except, $defaults)) : $defaults - ); - $instance = $this->newInstance(); $instance->setRawAttributes($attributes); $instance->fireModelEvent('replicating', false); - $definitions = $this->getRelationDefinitions(); - - foreach ($definitions as $type => $relations) { - foreach ($relations as $name => $options) { - if ($this->isRelationReplicable($name, $isMultisite, $isDuplicate)) { - $this->replicateRelationInternal($instance->$name(), $this->$name); - } - } - } - return $instance; } - - /** - * replicateRelationInternal on the model instance with the supplied ones - */ - protected function replicateRelationInternal($relationObject, $models) - { - if ($models instanceof CollectionBase) { - $models = $models->all(); - } - elseif ($models instanceof EloquentModel) { - $models = [$models]; - } - else { - $models = (array) $models; - } - - foreach (array_filter($models) as $model) { - if ($relationObject instanceof HasOneOrMany) { - $relationObject->add($model->replicateWithRelations()); - } - else { - $relationObject->add($model); - } - } - } - - /** - * isRelationReplicable determines whether the specified relation should be replicated - * when replicateWithRelations() is called instead of save() on the model. Default: true. - */ - protected function isRelationReplicable(string $name, bool $isMultisite, bool $isDuplicate): bool - { - $relationType = $this->getRelationType($name); - if ($relationType === 'morphTo') { - return false; - } - - // Relation is shared via propagation - if (!$isDuplicate && $isMultisite && $this->isAttributePropagatable($name)) { - return false; - } - - $definition = $this->getRelationDefinition($name); - if (!array_key_exists('replicate', $definition)) { - return true; - } - - return (bool) $definition['replicate']; - } } diff --git a/src/Database/DatabaseServiceProvider.php b/src/Database/DatabaseServiceProvider.php index 3defd800b..78520b6e3 100644 --- a/src/Database/DatabaseServiceProvider.php +++ b/src/Database/DatabaseServiceProvider.php @@ -72,6 +72,8 @@ protected function registerConnectionServices() return new DatabaseTransactionsManager; }); + $this->app->bind('db.replicator', Replicator::class); + $this->app->singleton('db.dongle', function ($app) { return new Dongle($this->getDefaultDatabaseDriver(), $app['db']); }); @@ -84,6 +86,6 @@ protected function getDefaultDatabaseDriver(): string { $defaultConnection = $this->app['db']->getDefaultConnection(); - return $this->app['config']['database.connections.' . $defaultConnection . '.driver']; + return $this->app['config']["database.connections.{$defaultConnection}.driver"]; } } diff --git a/src/Database/Replicator.php b/src/Database/Replicator.php new file mode 100644 index 000000000..1e0a998c7 --- /dev/null +++ b/src/Database/Replicator.php @@ -0,0 +1,153 @@ +model = $model; + $this->isMultisite = $model->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class); + } + + /** + * replicate replicates the model into a new, non-existing instance, + * including replicating relations. + * + * @param array|null $except + * @return static + */ + public function replicate(array $except = null) + { + $this->isDuplicating = false; + + return $this->replicateRelationsInternal($except); + } + + /** + * duplicate replicates a model with special multisite duplication logic. + * To avoid duplication of has many relations, the logic only propagates relations on + * the parent model since they are shared via site_root_id beyond this point. + * + * @param array|null $except + * @return static + */ + public function duplicate(array $except = null) + { + $this->isDuplicating = true; + + return $this->replicateRelationsInternal($except); + } + + /** + * replicateRelationsInternal + */ + protected function replicateRelationsInternal(array $except = null) + { + $defaults = [ + $this->model->getKeyName(), + $this->model->getCreatedAtColumn(), + $this->model->getUpdatedAtColumn(), + ]; + + if ($this->isMultisite) { + $defaults[] = 'site_root_id'; + } + + $attributes = Arr::except( + $this->model->attributes, + $except ? array_unique(array_merge($except, $defaults)) : $defaults + ); + + $instance = $this->model->newReplicationInstance($attributes); + + $definitions = $this->model->getRelationDefinitions(); + + foreach ($definitions as $type => $relations) { + foreach ($relations as $name => $options) { + if ($this->isRelationReplicable($name)) { + $this->replicateRelationInternal($instance->$name(), $this->model->$name); + } + } + } + + return $instance; + } + + /** + * replicateRelationInternal on the model instance with the supplied ones + */ + protected function replicateRelationInternal($relationObject, $models) + { + if ($models instanceof CollectionBase) { + $models = $models->all(); + } + elseif ($models instanceof EloquentModel) { + $models = [$models]; + } + else { + $models = (array) $models; + } + + foreach (array_filter($models) as $model) { + if ($relationObject instanceof HasOneOrMany) { + $relationObject->add($model->replicateWithRelations()); + } + else { + $relationObject->add($model); + } + } + } + + /** + * isRelationReplicable determines whether the specified relation should be replicated + * when replicateWithRelations() is called instead of save() on the model. Default: true. + */ + protected function isRelationReplicable(string $name): bool + { + $relationType = $this->model->getRelationType($name); + if ($relationType === 'morphTo') { + return false; + } + + // Relation is shared via propagation + if ( + !$this->isDuplicating && + $this->isMultisite && + $this->model->isAttributePropagatable($name) + ) { + return false; + } + + $definition = $this->model->getRelationDefinition($name); + if (!array_key_exists('replicate', $definition)) { + return true; + } + + return (bool) $definition['replicate']; + } +} From 99bbedd9a7f71bdd5a91e01ef2cce6691fe26b27 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 14 Feb 2024 13:40:58 +1100 Subject: [PATCH 61/74] Update tree associations when replicating --- src/Database/Replicator.php | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Database/Replicator.php b/src/Database/Replicator.php index 1e0a998c7..e61f864a0 100644 --- a/src/Database/Replicator.php +++ b/src/Database/Replicator.php @@ -25,6 +25,11 @@ class Replicator */ protected $isMultisite = false; + /** + * @var array associationMap from original record to newly created record + */ + protected $associationMap = []; + /** * __construct */ @@ -113,14 +118,21 @@ protected function replicateRelationInternal($relationObject, $models) $models = (array) $models; } + $this->associationMap = []; foreach (array_filter($models) as $model) { if ($relationObject instanceof HasOneOrMany) { - $relationObject->add($model->replicateWithRelations()); + $relationObject->add($newModel = $model->replicateWithRelations()); + $this->mapAssociation($model, $newModel); } else { $relationObject->add($model); } } + + $relatedModel = $relationObject->getRelated(); + if ($relatedModel->isClassInstanceOf(\October\Contracts\Database\TreeInterface::class)) { + $this->updateTreeAssociations(); + } } /** @@ -150,4 +162,27 @@ protected function isRelationReplicable(string $name): bool return (bool) $definition['replicate']; } + + /** + * mapAssociation is an internal method that keeps a record of what records were created + * and their associated source, the following format is used: + * + * [\Model\Class][1] => [FromModel, ToModel] + */ + protected function mapAssociation($currentModel, $replicatedModel) + { + $this->associationMap[$currentModel->getKey()] = [$currentModel, $replicatedModel]; + } + + /** + * updateTreeAssociations + */ + protected function updateTreeAssociations() + { + foreach ($this->associationMap as $tuple) { + [$currentModel, $replicatedModel] = $tuple; + $newParent = $this->associationMap[$currentModel->getParentId()][1] ?? null; + $replicatedModel->parent = $newParent; + } + } } From e61f195d1ea322778786c3b1ab2288663a6f4bc9 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 14 Feb 2024 18:32:51 +1100 Subject: [PATCH 62/74] Upgrading RainLab.User, docblocks --- helpers/Auth.php | 2 +- src/Database/Concerns/HasEagerLoadAttachRelation.php | 4 +++- src/Database/Replicator.php | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/helpers/Auth.php b/helpers/Auth.php index 077e9f178..5e25cab1b 100644 --- a/helpers/Auth.php +++ b/helpers/Auth.php @@ -3,6 +3,6 @@ /** * Auth * - * @see \Responsiv\User\Classes\AuthManager + * @see \RainLab\User\Classes\AuthManager */ class Auth extends October\Rain\Support\Facades\Auth {} diff --git a/src/Database/Concerns/HasEagerLoadAttachRelation.php b/src/Database/Concerns/HasEagerLoadAttachRelation.php index 4dbaa0391..55d3a88ba 100644 --- a/src/Database/Concerns/HasEagerLoadAttachRelation.php +++ b/src/Database/Concerns/HasEagerLoadAttachRelation.php @@ -3,7 +3,9 @@ use Closure; /** - * HasNicerPagination for a query builder + * HasEagerLoadAttachRelation eagerly loads all attachments on a model in one pass. + * Since they share a common type and database table, multiple attachment definitions + * can be eagerly loaded as a single query. */ trait HasEagerLoadAttachRelation { diff --git a/src/Database/Replicator.php b/src/Database/Replicator.php index e61f864a0..135ed4321 100644 --- a/src/Database/Replicator.php +++ b/src/Database/Replicator.php @@ -167,7 +167,7 @@ protected function isRelationReplicable(string $name): bool * mapAssociation is an internal method that keeps a record of what records were created * and their associated source, the following format is used: * - * [\Model\Class][1] => [FromModel, ToModel] + * [FromModel::id] => [FromModel, ToModel] */ protected function mapAssociation($currentModel, $replicatedModel) { @@ -175,7 +175,7 @@ protected function mapAssociation($currentModel, $replicatedModel) } /** - * updateTreeAssociations + * updateTreeAssociations sets new parents on the replicated records */ protected function updateTreeAssociations() { From 499001c24ccf062b544275dfad1e53963582b62c Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 14 Feb 2024 22:01:53 +1100 Subject: [PATCH 63/74] Adds credit for Sergey Kasyanov --- CREDITS.md | 3 +++ src/Support/Facades/Auth.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CREDITS.md b/CREDITS.md index 4e270b05d..ecb7995d9 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -16,3 +16,6 @@ https://github.com/jakeasmith/http_build_url "Twig extensions", Copyright (c) 2016 Vojta Svoboda https://github.com/vojtasvoboda/oc-twigextensions-plugin + +"October Code", Copyright (c) 2022 Sergey Kasyanov +https://github.com/SergeyKasyanov/vscode-october-extension diff --git a/src/Support/Facades/Auth.php b/src/Support/Facades/Auth.php index 10141ec83..2a8a2dd43 100644 --- a/src/Support/Facades/Auth.php +++ b/src/Support/Facades/Auth.php @@ -5,7 +5,7 @@ /** * Auth * - * @see \User\Classes\AuthManager + * @see \RainLab\User\Classes\AuthManager */ class Auth extends AuthBase { From 770eb6db3d94142a8a9d713e02ad052c261180f3 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sun, 18 Feb 2024 12:39:18 +1100 Subject: [PATCH 64/74] Adds sync config Now you can sync between locale, group and "all" --- src/Database/Traits/Multisite.php | 49 ++++++++++++++++++++++++++----- src/Support/Facades/Site.php | 3 +- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/Database/Traits/Multisite.php b/src/Database/Traits/Multisite.php index d5297992c..3aa37cf3f 100644 --- a/src/Database/Traits/Multisite.php +++ b/src/Database/Traits/Multisite.php @@ -16,13 +16,20 @@ trait Multisite /** * @var array propagatable list of attributes to propagate to other sites. * - * protected $propagatable = []; + * protected $propagatable = []; */ /** - * @var bool propagatableSync will enforce model structures between all sites + * @var bool|array propagatableSync will enforce model structures between all sites. + * When set to `false` will disable sync, set `true` will sync between the site group. + * The sync option allow sync to `all` sites, sites in the `group`, and sites the `locale`. * - * protected $propagatableSync = false; + * Set to an array of options for more granular controls: + * + * - **sync** - logic to sync specific sites, available options: `all`, `group`, `locale` + * - **delete** - delete all linked records when any record is deleted, default: `true` + * + * protected $propagatableSync = false; */ /** @@ -239,9 +246,27 @@ public function isMultisiteEnabled() */ public function isMultisiteSyncEnabled() { - return property_exists($this, 'propagatableSync') - ? (bool) $this->propagatableSync - : false; + if (!property_exists($this, 'propagatableSync')) { + return false; + } + + if (!is_array($this->propagatableSync)) { + return ($this->propagatableSync['sync'] ?? false) !== false; + } + + return (bool) $this->propagatableSync; + } + + /** + * getMultisiteConfig + */ + public function getMultisiteConfig($key, $default = null) + { + if (!property_exists($this, 'propagatableSync') || !is_array($this->propagatableSync)) { + return $default; + } + + return array_get($this->propagatableSync, $key, $default); } /** @@ -250,7 +275,17 @@ public function isMultisiteSyncEnabled() */ public function getMultisiteSyncSites() { - return Site::listSiteIdsInContext(); + if ($this->getMultisiteConfig('sync') === 'all') { + return Site::listSiteIds(); + } + + $siteId = $this->{$this->getSiteIdColumn()} ?: null; + + if ($this->getMultisiteConfig('sync') === 'locale') { + return Site::listSiteIdsInLocale($siteId); + } + + return Site::listSiteIdsInGroup($siteId); } /** diff --git a/src/Support/Facades/Site.php b/src/Support/Facades/Site.php index 9ccbd6c42..64ae03770 100644 --- a/src/Support/Facades/Site.php +++ b/src/Support/Facades/Site.php @@ -12,7 +12,8 @@ * @method static bool hasMultiSite() * @method static array listEnabled() * @method static array listSiteIds() - * @method static array listSiteIdsInContext() + * @method static array listSiteIdsInGroup($siteId) + * @method static array listSiteIdsInLocale($siteId) * @method static iterable listSites() * @method static int|null getSiteIdFromContext() * @method static mixed getSiteFromContext() From cdf435af1cc1fea20079059312870533b1fbdecd Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sun, 18 Feb 2024 13:59:09 +1100 Subject: [PATCH 65/74] Propagate multisite deletes --- src/Database/Traits/Multisite.php | 44 +++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/Database/Traits/Multisite.php b/src/Database/Traits/Multisite.php index 3aa37cf3f..815702078 100644 --- a/src/Database/Traits/Multisite.php +++ b/src/Database/Traits/Multisite.php @@ -58,6 +58,8 @@ public function initializeMultisite() $this->bindEvent('model.saveComplete', [$this, 'multisiteSaveComplete']); + $this->bindEvent('model.afterDelete', [$this, 'multisiteAfterDelete']); + $this->defineMultisiteRelations(); } @@ -123,6 +125,24 @@ public function multisiteAfterCreate() ; } + /** + * multisiteAfterDelete + */ + public function multisiteAfterDelete() + { + if (!$this->isMultisiteSyncEnabled() || !$this->getMultisiteConfig('delete', true)) { + return; + } + + Site::withGlobalContext(function() { + foreach ($this->getMultisiteSyncSites() as $siteId) { + if (!$this->isModelUsingSameSite($siteId)) { + $this->deleteForSite($siteId); + } + } + }); + } + /** * defineMultisiteRelations will spin over every relation and apply propagation config */ @@ -376,6 +396,30 @@ protected function findOtherSiteModel($siteId = null) return $otherModel; } + /** + * deleteForSite runs the delete command on a model for another site, useful for cleaning + * up records for other sites when the parent is deleted. + */ + public function deleteForSite($siteId = null) + { + $otherModel = $this->findForSite($siteId); + if (!$otherModel) { + return; + } + + $useSoftDeletes = $this->isClassInstanceOf(\October\Contracts\Database\SoftDeleteInterface::class); + if ($useSoftDeletes && !$this->isSoftDelete()) { + static::withoutEvents(function() use ($otherModel) { + $otherModel->forceDelete(); + }); + return; + } + + static::withoutEvents(function() use ($otherModel) { + $otherModel->delete(); + }); + } + /** * isModelUsingSameSite */ From c6edb411349fea6f158fef71245615fd3d3f7d0a Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Tue, 20 Feb 2024 17:58:42 +1100 Subject: [PATCH 66/74] Detect and preserve shared multisite relationships --- src/Database/Concerns/HasRelationships.php | 51 +++++++++++++--------- src/Database/Traits/Multisite.php | 26 ++++++++++- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/Database/Concerns/HasRelationships.php b/src/Database/Concerns/HasRelationships.php index 60f132402..6e65fb53f 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -876,42 +876,51 @@ protected function setRelationSimpleValue($relationName, $value) } /** - * performDeleteOnRelations locates relations with delete flag and cascades - * the delete event. + * performDeleteOnRelations locates relations with delete flag and cascades the + * delete event. This is called before the parent model is deleted. This method + * checks in with the Multisite trait to preserve shared relations. + * + * @see \October\Rain\Database\Traits\Multisite::canDeleteMultisiteRelation */ protected function performDeleteOnRelations() { $definitions = $this->getRelationDefinitions(); + $useMultisite = $this->isClassInstanceOf(\October\Contracts\Database\MultisiteInterface::class) && $this->isMultisiteEnabled(); + foreach ($definitions as $type => $relations) { - // Hard 'delete' definition foreach ($relations as $name => $options) { - if (!Arr::get($options, 'delete', false)) { - continue; - } - - if (!$relation = $this->{$name}) { + // Detect and preserve shared multisite relationships + if ($useMultisite && !$this->canDeleteMultisiteRelation($name, $type)) { continue; } - if ($relation instanceof EloquentModel) { - $relation->forceDelete(); - } - elseif ($relation instanceof CollectionBase) { - $relation->each(function ($model) { - $model->forceDelete(); - }); - } - } - - // Belongs-To-Many should clean up after itself by default - if ($type === 'belongsToMany') { - foreach ($relations as $name => $options) { + // Belongs-To-Many should clean up after itself by default + if ($type === 'belongsToMany') { if (!Arr::get($options, 'detach', true)) { return; } $this->{$name}()->detach(); } + // Hard 'delete' definition + else { + if (!Arr::get($options, 'delete', false)) { + continue; + } + + if (!$relation = $this->{$name}) { + continue; + } + + if ($relation instanceof EloquentModel) { + $relation->forceDelete(); + } + elseif ($relation instanceof CollectionBase) { + $relation->each(function ($model) { + $model->forceDelete(); + }); + } + } } } } diff --git a/src/Database/Traits/Multisite.php b/src/Database/Traits/Multisite.php index 815702078..b25fc9450 100644 --- a/src/Database/Traits/Multisite.php +++ b/src/Database/Traits/Multisite.php @@ -158,7 +158,31 @@ protected function defineMultisiteRelations() } /** - * defineMultisiteRelation + * canDeleteMultisiteRelation checks if a relation has the potential to be shared with + * the current model. If there are 2 or more records in existence, then this method + * will prevent the cascading deletion of relations. + * + * @see \October\Rain\Database\Concerns\HasRelationships::performDeleteOnRelations + */ + public function canDeleteMultisiteRelation($name, $type = null): bool + { + if ($type === null) { + $type = $this->getRelationType($name); + } + + if (!in_array($type, ['belongsToMany', 'belongsTo', 'hasOne', 'hasMany', 'attachOne', 'attachMany'])) { + return false; + } + + // The current record counts for one so halt if we find more + return !($this->newOtherSiteQuery()->count() > 1); + } + + /** + * defineMultisiteRelation will modify defined relations on this model so they share + * their association using the shared identifier (`site_root_id`). Only these relation + * types support relation sharing: `belongsToMany`, `belongsTo`, `hasOne`, `hasMany`, + * `attachOne`, `attachMany`. */ protected function defineMultisiteRelation($name, $type = null) { From e8dafc73dae2d2c116252e298528d6c6f9bd96a3 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 21 Feb 2024 09:40:12 +1100 Subject: [PATCH 67/74] Review logic --- src/Database/Traits/Multisite.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Database/Traits/Multisite.php b/src/Database/Traits/Multisite.php index b25fc9450..332c86be2 100644 --- a/src/Database/Traits/Multisite.php +++ b/src/Database/Traits/Multisite.php @@ -166,6 +166,10 @@ protected function defineMultisiteRelations() */ public function canDeleteMultisiteRelation($name, $type = null): bool { + if (!$this->isAttributePropagatable($name)) { + return false; + } + if ($type === null) { $type = $this->getRelationType($name); } From d200114886af0f18254e0a467e39be347f78fdbf Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 21 Feb 2024 14:05:53 +1100 Subject: [PATCH 68/74] Fixes duplication of defined constraints The custom query logic already has these defined by the relation constructor --- src/Database/Relations/DeferOneOrMany.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Database/Relations/DeferOneOrMany.php b/src/Database/Relations/DeferOneOrMany.php index 3a6c24257..4bc3a9d87 100644 --- a/src/Database/Relations/DeferOneOrMany.php +++ b/src/Database/Relations/DeferOneOrMany.php @@ -61,9 +61,8 @@ public function withDeferred($sessionKey = null) // Trick the relation to add constraints to this nested query $this->query = $query; $this->addConstraints(); + $this->addDefinedConstraintsToQuery($this); } - - $this->addDefinedConstraintsToQuery($this); } // Bind (Add) From 5a399b553c672e97f70383eb323546971651fc6b Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 21 Feb 2024 15:30:30 +1100 Subject: [PATCH 69/74] Make pivot data available to existing records via withDeferred This uses a left join instead of custom queries to include and exclude deferred records. This is more efficient and a step needed for deferring a sort_order column --- src/Database/Relations/BelongsToMany.php | 33 +++++++++++++++------ src/Database/Relations/DeferOneOrMany.php | 35 +++++------------------ src/Database/Relations/MorphToMany.php | 8 ++---- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/src/Database/Relations/BelongsToMany.php b/src/Database/Relations/BelongsToMany.php index b52186eb3..7a4f4ed5c 100644 --- a/src/Database/Relations/BelongsToMany.php +++ b/src/Database/Relations/BelongsToMany.php @@ -23,11 +23,6 @@ class BelongsToMany extends BelongsToManyBase */ public $countMode = false; - /** - * @var bool orphanMode used when a join is not used, don't select aliased columns - */ - public $orphanMode = false; - /** * __construct a new belongs to many relationship instance. * @@ -60,12 +55,36 @@ public function __construct( $this->addDefinedConstraints(); } + /** + * performJoin will join the pivot table opportunistically instead of mandatorily + * to support deferred bindings that exist in another table. + * + * This method is based on `performJoin` method logic except it uses a left join. + * + * @param \Illuminate\Database\Eloquent\Builder|null $query + * @return $this + */ + protected function performLeftJoin($query = null) + { + $query = $query ?: $this->query; + + $query->leftJoin( + $this->table, + $this->getQualifiedRelatedKeyName(), + '=', + $this->getQualifiedRelatedPivotKeyName() + ); + + return $this; + } + /** * shouldSelect gets the select columns for the relation query * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany */ protected function shouldSelect(array $columns = ['*']) { + // @deprecated remove this whole method when `countMode` is gone if ($this->countMode) { return $this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->foreignPivotKey; } @@ -74,10 +93,6 @@ protected function shouldSelect(array $columns = ['*']) $columns = [$this->related->getTable().'.*']; } - if ($this->orphanMode) { - return $columns; - } - return array_merge($columns, $this->aliasedPivotColumns()); } diff --git a/src/Database/Relations/DeferOneOrMany.php b/src/Database/Relations/DeferOneOrMany.php index 4bc3a9d87..068682ee5 100644 --- a/src/Database/Relations/DeferOneOrMany.php +++ b/src/Database/Relations/DeferOneOrMany.php @@ -1,7 +1,7 @@ parent->sessionKey; } - // No join table will be used, strip the selected "pivot_" columns + // Swap the standard inner join for a left join if ($this instanceof BelongsToManyBase) { - $this->orphanMode = true; + $this->performLeftJoin($newQuery); } $newQuery->where(function ($query) use ($sessionKey) { if ($this->parent->exists) { - if ($this instanceof MorphToMany) { - // Custom query for MorphToMany since a "join" cannot be used - $query->whereExists(function ($query) { - $query - ->select($this->parent->getConnection()->raw(1)) - ->from($this->table) - ->where($this->getQualifiedRelatedPivotKeyName(), DbDongle::raw(DbDongle::getTablePrefix().$this->related->getQualifiedKeyName())) - ->where($this->getQualifiedForeignPivotKeyName(), $this->parent->getKey()) - ->where($this->getMorphType(), $this->getMorphClass()); - }); - } - elseif ($this instanceof BelongsToManyBase) { - // Custom query for BelongsToManyBase since a "join" cannot be used - $query->whereExists(function ($query) { - $query - ->select($this->parent->getConnection()->raw(1)) - ->from($this->table) - ->where($this->getQualifiedRelatedPivotKeyName(), DbDongle::raw(DbDongle::getTablePrefix().$this->related->getQualifiedKeyName())) - ->where($this->getQualifiedForeignPivotKeyName(), $this->parent->getKey()); - }); - } - else { - // Trick the relation to add constraints to this nested query - $this->query = $query; - $this->addConstraints(); + $this->query = $query; + $this->addConstraints(); + + if (!$this instanceof BelongsToManyBase) { $this->addDefinedConstraintsToQuery($this); } } diff --git a/src/Database/Relations/MorphToMany.php b/src/Database/Relations/MorphToMany.php index c96f7e9ef..8c5b96e87 100644 --- a/src/Database/Relations/MorphToMany.php +++ b/src/Database/Relations/MorphToMany.php @@ -146,14 +146,10 @@ public function newPivotQuery() */ public function newPivot(array $attributes = [], $exists = false) { - /* - * October looks to the relationship parent - */ + // October looks to the relationship parent $pivot = $this->parent->newRelationPivot($this->relationName, $this->parent, $attributes, $this->table, $exists); - /* - * Laravel creates new pivot model this way - */ + // Laravel creates new pivot model this way if (empty($pivot)) { $using = $this->using; From 58fcf6be8ad78815a47829d2977d6e69fda45840 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 21 Feb 2024 20:24:24 +1100 Subject: [PATCH 70/74] Adds sort_order support for deferred binding --- src/Database/Relations/BelongsToMany.php | 149 ++++++++++++++++------ src/Database/Relations/DeferOneOrMany.php | 36 ++++-- src/Database/Traits/SortableRelation.php | 15 ++- 3 files changed, 147 insertions(+), 53 deletions(-) diff --git a/src/Database/Relations/BelongsToMany.php b/src/Database/Relations/BelongsToMany.php index 7a4f4ed5c..b63512829 100644 --- a/src/Database/Relations/BelongsToMany.php +++ b/src/Database/Relations/BelongsToMany.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection as CollectionBase; use Illuminate\Database\Eloquent\Relations\BelongsToMany as BelongsToManyBase; +use October\Rain\Support\Facades\DbDongle; /** * BelongsToMany @@ -55,47 +56,6 @@ public function __construct( $this->addDefinedConstraints(); } - /** - * performJoin will join the pivot table opportunistically instead of mandatorily - * to support deferred bindings that exist in another table. - * - * This method is based on `performJoin` method logic except it uses a left join. - * - * @param \Illuminate\Database\Eloquent\Builder|null $query - * @return $this - */ - protected function performLeftJoin($query = null) - { - $query = $query ?: $this->query; - - $query->leftJoin( - $this->table, - $this->getQualifiedRelatedKeyName(), - '=', - $this->getQualifiedRelatedPivotKeyName() - ); - - return $this; - } - - /** - * shouldSelect gets the select columns for the relation query - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - protected function shouldSelect(array $columns = ['*']) - { - // @deprecated remove this whole method when `countMode` is gone - if ($this->countMode) { - return $this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->foreignPivotKey; - } - - if ($columns === ['*']) { - $columns = [$this->related->getTable().'.*']; - } - - return array_merge($columns, $this->aliasedPivotColumns()); - } - /** * save the supplied related model with deferred binding support. */ @@ -467,4 +427,111 @@ public function getOtherKey() { return $this->table.'.'.$this->relatedPivotKey; } + + /** + * shouldSelect gets the select columns for the relation query + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + protected function shouldSelect(array $columns = ['*']) + { + // @deprecated remove this whole method when `countMode` is gone + if ($this->countMode) { + return $this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->foreignPivotKey; + } + + if ($columns === ['*']) { + $columns = [$this->related->getTable().'.*']; + } + + return array_merge($columns, $this->aliasedPivotColumns()); + } + + /** + * performJoin will join the pivot table opportunistically instead of mandatorily + * to support deferred bindings that exist in another table. + * + * This method is based on `performJoin` method logic except it uses a left join. + * + * @param \Illuminate\Database\Eloquent\Builder|null $query + * @return $this + */ + protected function performLeftJoin($query = null) + { + $query = $query ?: $this->query; + + $query->leftJoin( + $this->table, + $this->getQualifiedRelatedKeyName(), + '=', + $this->getQualifiedRelatedPivotKeyName() + ); + + return $this; + } + + /** + * performSortableColumnJoin includes custom logic to replace the sort order column with + * a unified column + */ + protected function performSortableColumnJoin($query = null, $sessionKey = null) + { + if ( + !$this->parent->isClassInstanceOf(\October\Contracts\Database\SortableRelationInterface::class) || + !$this->parent->isSortableRelation($this->relationName) + ) { + return; + } + + // Check if sorting by the matched sort_order column + $sortColumn = $this->qualifyPivotColumn( + $this->parent->getRelationSortOrderColumn($this->relationName) + ); + + $orderDefinitions = $query->getQuery()->orders; + + traceLog($orderDefinitions); + + if (!is_array($orderDefinitions)) { + return; + } + + $sortableIndex = false; + foreach ($orderDefinitions as $index => $order) { + if ($order['column'] === $sortColumn) { + $sortableIndex = $index; + } + } + + // Not sorting by the sort column, abort + if ($sortableIndex === false) { + return; + } + + // Join the deferred binding table and select the combo column + $tempOrderColumns = 'october_reserved_sort_order'; + $combinedOrderColumn = "ifnull(deferred_bindings.sort_order, {$sortColumn}) as {$tempOrderColumns}"; + $this->performDeferredLeftJoin($query, $sessionKey); + $this->addSelect(DbDongle::raw($combinedOrderColumn)); + + // Overwrite the sortable column with the combined one + $query->getQuery()->orders[$sortableIndex]['column'] = $tempOrderColumns; + } + + /** + * performDeferredLeftJoin left joins the deferred bindings table + */ + protected function performDeferredLeftJoin($query = null, $sessionKey = null) + { + $query = $query ?: $this->query; + + $query->leftJoin('deferred_bindings', function($join) use ($sessionKey) { + $join->on( + $this->getQualifiedRelatedKeyName(), '=', 'deferred_bindings.slave_id') + ->where('master_field', $this->relationName) + ->where('master_type', get_class($this->parent)) + ->where('session_key', $sessionKey); + }); + + return $this; + } } diff --git a/src/Database/Relations/DeferOneOrMany.php b/src/Database/Relations/DeferOneOrMany.php index 068682ee5..16893618b 100644 --- a/src/Database/Relations/DeferOneOrMany.php +++ b/src/Database/Relations/DeferOneOrMany.php @@ -18,11 +18,28 @@ trait DeferOneOrMany */ public function withDeferred($sessionKey = null) { - $modelQuery = $this->query; + $newQuery = $this->query->getQuery()->newQuery(); + $newQuery->from($this->related->getTable()); - $newQuery = $modelQuery->getQuery()->newQuery(); + // Readd the defined constraints + $this->addDefinedConstraintsToQuery($newQuery); - $newQuery->from($this->related->getTable()); + return $this->withDeferredQuery($newQuery, $sessionKey); + } + + /** + * withDeferredQuery returns the model query with deferred bindings added + * @param \Illuminate\Database\Query\Builder $newQuery + * @param string|null $sessionKey + * @return \Illuminate\Database\Query\Builder + */ + public function withDeferredQuery($newQuery = null, $sessionKey = null) + { + // Use case here is not wanting addDefinedConstraintsToQuery + if ($newQuery === null) { + $newQuery = $this->query->getQuery()->newQuery(); + $newQuery->from($this->related->getTable()); + } // Guess the key from the parent model if ($sessionKey === null) { @@ -32,16 +49,14 @@ public function withDeferred($sessionKey = null) // Swap the standard inner join for a left join if ($this instanceof BelongsToManyBase) { $this->performLeftJoin($newQuery); + $this->performSortableColumnJoin($newQuery, $sessionKey); } $newQuery->where(function ($query) use ($sessionKey) { + // Trick the relation to add constraints to this nested query if ($this->parent->exists) { $this->query = $query; $this->addConstraints(); - - if (!$this instanceof BelongsToManyBase) { - $this->addDefinedConstraintsToQuery($this); - } } // Bind (Add) @@ -79,14 +94,15 @@ public function withDeferred($sessionKey = null) ]); }); - $modelQuery->setQuery($newQuery); + // Bless this query with the deferred query + $this->query->setQuery($newQuery); // Apply global scopes foreach ($this->related->getGlobalScopes() as $identifier => $scope) { - $modelQuery->withGlobalScope($identifier, $scope); + $this->query->withGlobalScope($identifier, $scope); } - return $this->query = $modelQuery; + return $this->query; } /** diff --git a/src/Database/Traits/SortableRelation.php b/src/Database/Traits/SortableRelation.php index d9df334e6..9b470f8e2 100644 --- a/src/Database/Traits/SortableRelation.php +++ b/src/Database/Traits/SortableRelation.php @@ -1,5 +1,6 @@ $relationName()->updateExistingPivot($update['id'], [ + $result = $this->exists ? $this->$relationName()->updateExistingPivot($update['id'], [ $this->getRelationSortOrderColumn($relationName) => $update['sort_order'] - ]); + ]) : 0; + + if (!$result && $this->sessionKey) { + Db::table('deferred_bindings') + ->where('master_field', $relationName) + ->where('master_type', get_class($this)) + ->where('session_key', $this->sessionKey) + ->where('slave_id', $update['id']) + ->limit(1) + ->update(['sort_order' => $update['sort_order']]); + } } } } From 2ef7f8f455361d7d4d98e823aa1fc20a515e9c42 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 21 Feb 2024 20:58:06 +1100 Subject: [PATCH 71/74] Refactor withDeferredQuery interface --- src/Database/Relations/DeferOneOrMany.php | 37 +++++++++++++---------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/Database/Relations/DeferOneOrMany.php b/src/Database/Relations/DeferOneOrMany.php index 16893618b..9eb3991e7 100644 --- a/src/Database/Relations/DeferOneOrMany.php +++ b/src/Database/Relations/DeferOneOrMany.php @@ -12,7 +12,8 @@ trait DeferOneOrMany { /** - * withDeferred returns the model query with deferred bindings added + * withDeferred returns a new model query with deferred bindings added, this + * will reset any constraints that come before it * @param string|null $sessionKey * @return \Illuminate\Database\Query\Builder */ @@ -24,21 +25,31 @@ public function withDeferred($sessionKey = null) // Readd the defined constraints $this->addDefinedConstraintsToQuery($newQuery); - return $this->withDeferredQuery($newQuery, $sessionKey); + // Apply deferred binding to the new query + $newQuery = $this->withDeferredQuery($newQuery, $sessionKey); + + // Bless this query with the deferred query + $this->query->setQuery($newQuery); + + // Readd the global scopes + foreach ($this->related->getGlobalScopes() as $identifier => $scope) { + $this->query->withGlobalScope($identifier, $scope); + } + + return $this->query; } /** - * withDeferredQuery returns the model query with deferred bindings added - * @param \Illuminate\Database\Query\Builder $newQuery + * withDeferredQuery returns the supplied model query, or current model query, with + * deferred bindings added, this will preserve any constraints that came before it + * @param \Illuminate\Database\Query\Builder|null $newQuery * @param string|null $sessionKey * @return \Illuminate\Database\Query\Builder */ public function withDeferredQuery($newQuery = null, $sessionKey = null) { - // Use case here is not wanting addDefinedConstraintsToQuery if ($newQuery === null) { - $newQuery = $this->query->getQuery()->newQuery(); - $newQuery->from($this->related->getTable()); + $newQuery = $this->query->getQuery(); } // Guess the key from the parent model @@ -55,8 +66,10 @@ public function withDeferredQuery($newQuery = null, $sessionKey = null) $newQuery->where(function ($query) use ($sessionKey) { // Trick the relation to add constraints to this nested query if ($this->parent->exists) { + $oldQuery = $this->query; $this->query = $query; $this->addConstraints(); + $this->query = $oldQuery; } // Bind (Add) @@ -94,15 +107,7 @@ public function withDeferredQuery($newQuery = null, $sessionKey = null) ]); }); - // Bless this query with the deferred query - $this->query->setQuery($newQuery); - - // Apply global scopes - foreach ($this->related->getGlobalScopes() as $identifier => $scope) { - $this->query->withGlobalScope($identifier, $scope); - } - - return $this->query; + return $newQuery; } /** From 28539e6116329746fcb95240780f4ddc91d31cf4 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 21 Feb 2024 21:51:02 +1100 Subject: [PATCH 72/74] Commit deferred sort orders, bug fix in join logic --- src/Database/Concerns/HasReplication.php | 3 +++ src/Database/Models/DeferredBinding.php | 11 ++++++++++- src/Database/Relations/BelongsToMany.php | 12 ++++-------- src/Database/Traits/DeferredBinding.php | 5 +++-- src/Database/Traits/SortableRelation.php | 9 ++++++--- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/Database/Concerns/HasReplication.php b/src/Database/Concerns/HasReplication.php index 80dbdc4bd..6a3d1bcf6 100644 --- a/src/Database/Concerns/HasReplication.php +++ b/src/Database/Concerns/HasReplication.php @@ -4,6 +4,9 @@ /** * HasReplication for a model + * + * @package october\database + * @author Alexey Bobkov, Samuel Georges */ trait HasReplication { diff --git a/src/Database/Models/DeferredBinding.php b/src/Database/Models/DeferredBinding.php index 30cedbfa8..1fbc2500b 100644 --- a/src/Database/Models/DeferredBinding.php +++ b/src/Database/Models/DeferredBinding.php @@ -59,7 +59,7 @@ public function beforeCreate() * getPivotDataForBind strips attributes beginning with an underscore, allowing * meta data to be stored using the column alongside the data. */ - public function getPivotDataForBind(): array + public function getPivotDataForBind($model, $relationName): array { $data = []; @@ -70,6 +70,15 @@ public function getPivotDataForBind(): array $data[$key] = $value; } + + if ( + $model->isClassInstanceOf(\October\Contracts\Database\SortableRelationInterface::class) && + $model->isSortableRelation($relationName) + ) { + $sortColumn = $model->getRelationSortOrderColumn($relationName); + $data[$sortColumn] = $this->sort_order; + } + return $data; } diff --git a/src/Database/Relations/BelongsToMany.php b/src/Database/Relations/BelongsToMany.php index b63512829..6309dcf43 100644 --- a/src/Database/Relations/BelongsToMany.php +++ b/src/Database/Relations/BelongsToMany.php @@ -459,12 +459,10 @@ protected function performLeftJoin($query = null) { $query = $query ?: $this->query; - $query->leftJoin( - $this->table, - $this->getQualifiedRelatedKeyName(), - '=', - $this->getQualifiedRelatedPivotKeyName() - ); + $query->leftJoin($this->table, function($join) { + $join->on($this->getQualifiedRelatedKeyName(), '=', $this->getQualifiedRelatedPivotKeyName()); + $join->where($this->getQualifiedForeignPivotKeyName(), $this->parent->getKey()); + }); return $this; } @@ -489,8 +487,6 @@ protected function performSortableColumnJoin($query = null, $sessionKey = null) $orderDefinitions = $query->getQuery()->orders; - traceLog($orderDefinitions); - if (!is_array($orderDefinitions)) { return; } diff --git a/src/Database/Traits/DeferredBinding.php b/src/Database/Traits/DeferredBinding.php index 3af09366b..3c94e0a92 100644 --- a/src/Database/Traits/DeferredBinding.php +++ b/src/Database/Traits/DeferredBinding.php @@ -3,7 +3,7 @@ use October\Rain\Database\Models\DeferredBinding as DeferredBindingModel; /** - * DeferredBinding trait + * DeferredBinding trait is implemented by all models * * @package october\database * @author Alexey Bobkov, Samuel Georges @@ -196,7 +196,8 @@ protected function commitDeferredOfType($sessionKey, $include = null, $exclude = $relationObj = $this->$relationName(); if ($binding->is_bind) { if (in_array($relationType, ['belongsToMany', 'morphToMany', 'morphedByMany'])) { - $relationObj->add($slaveModel, null, $binding->getPivotDataForBind()); + $pivotData = $binding->getPivotDataForBind($this, $relationName); + $relationObj->add($slaveModel, null, $pivotData); } else { $relationObj->add($slaveModel); diff --git a/src/Database/Traits/SortableRelation.php b/src/Database/Traits/SortableRelation.php index 9b470f8e2..b27195301 100644 --- a/src/Database/Traits/SortableRelation.php +++ b/src/Database/Traits/SortableRelation.php @@ -42,12 +42,15 @@ public function initializeSortableRelation() return; } - $relation = $this->$relationName(); - + // Order already set in pivot data (assuming singular) $column = $this->getRelationSortOrderColumn($relationName); + if (is_array($data) && array_key_exists($column, $data)) { + return; + } + // Calculate a new order + $relation = $this->$relationName(); $order = $relation->max($relation->qualifyPivotColumn($column)); - foreach ((array) $attached as $id) { $relation->updateExistingPivot($id, [$column => ++$order]); } From f05d8753674d9da2abc6a76e81fdd4073e58e455 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Wed, 21 Feb 2024 22:10:04 +1100 Subject: [PATCH 73/74] Adds sort_order to deferred_bindings table --- ...2013_10_01_000001_Db_Deferred_Bindings.php | 2 ++ ...Db_Add_Pivot_Data_To_Deferred_Bindings.php | 21 ------------------- 2 files changed, 2 insertions(+), 21 deletions(-) delete mode 100644 src/Database/Migrations/2021_10_01_000004_Db_Add_Pivot_Data_To_Deferred_Bindings.php diff --git a/src/Database/Migrations/2013_10_01_000001_Db_Deferred_Bindings.php b/src/Database/Migrations/2013_10_01_000001_Db_Deferred_Bindings.php index 61528871c..aa0ed9ed1 100644 --- a/src/Database/Migrations/2013_10_01_000001_Db_Deferred_Bindings.php +++ b/src/Database/Migrations/2013_10_01_000001_Db_Deferred_Bindings.php @@ -14,7 +14,9 @@ public function up() $table->string('slave_type'); $table->integer('slave_id'); $table->string('session_key'); + $table->mediumText('pivot_data')->nullable(); $table->boolean('is_bind')->default(true); + $table->integer('sort_order')->nullable(); $table->timestamps(); }); } diff --git a/src/Database/Migrations/2021_10_01_000004_Db_Add_Pivot_Data_To_Deferred_Bindings.php b/src/Database/Migrations/2021_10_01_000004_Db_Add_Pivot_Data_To_Deferred_Bindings.php deleted file mode 100644 index 2b0238242..000000000 --- a/src/Database/Migrations/2021_10_01_000004_Db_Add_Pivot_Data_To_Deferred_Bindings.php +++ /dev/null @@ -1,21 +0,0 @@ -mediumText('pivot_data')->nullable()->after('slave_id'); - }); - } - - public function down() - { - Schema::table('deferred_bindings', function (Blueprint $table) { - $table->dropColumn('pivot_data'); - }); - } -}; From 24af0ca6b33c54ac134a19317e00e198ad802d55 Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Fri, 23 Feb 2024 10:09:36 +1100 Subject: [PATCH 74/74] Doc comments --- src/Database/Concerns/HasRelationships.php | 118 +++++++++++---------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/src/Database/Concerns/HasRelationships.php b/src/Database/Concerns/HasRelationships.php index 6e65fb53f..a8b2b90b0 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -19,22 +19,24 @@ use InvalidArgumentException; /** - * HasRelationships concern for a model, using a cleaner declaration of relationships. + * HasRelationships is a concern used by the \October\Rain\Database\Model class, employing a + * cleaner declaration of model relationships. * - * Uses a similar approach to the relation methods used by Eloquent, but as separate properties - * that make the class file less cluttered. + * The relation definitions uses an almost identical approach to the relation methods defined + * by Eloquent, instead using class properties to make the class file less cluttered and keep + * the logic separated from the definition. * - * It should be declared with keys as the relation name, and value being a mixed array. - * The relation type $morphTo does not include a class name as the first value. + * Relations should be declared with keys as the relation name and value as a mixed array. + * The relation type `$morphTo` does not include a class name as the first value. * * Example: * - * class Order extends Model - * { - * protected $hasMany = [ - * 'items' => Item::class - * ]; - * } + * class Order extends Model + * { + * protected $hasMany = [ + * 'items' => Item::class + * ]; + * } * * @package october\database * @author Alexey Bobkov, Samuel Georges @@ -44,9 +46,9 @@ trait HasRelationships /** * @var array hasOne related record, inverse of belongsTo. * - * protected $hasOne = [ - * 'owner' => [User::class, 'key' => 'user_id'] - * ]; + * protected $hasOne = [ + * 'owner' => [User::class, 'key' => 'user_id'] + * ]; * */ public $hasOne = []; @@ -54,104 +56,108 @@ trait HasRelationships /** * @var array hasMany related records, inverse of belongsTo. * - * protected $hasMany = [ - * 'items' => Item::class - * ]; + * protected $hasMany = [ + * 'items' => Item::class + * ]; */ public $hasMany = []; /** * @var array belongsTo another record with a local key attribute * - * protected $belongsTo = [ - * 'parent' => [Category::class, 'key' => 'parent_id'] - * ]; + * protected $belongsTo = [ + * 'parent' => [Category::class, 'key' => 'parent_id'] + * ]; */ public $belongsTo = []; /** * @var array belongsToMany to multiple records using a join table. * - * protected $belongsToMany = [ - * 'groups' => [Group::class, 'table'=> 'join_groups_users'] - * ]; + * protected $belongsToMany = [ + * 'groups' => [Group::class, 'table'=> 'join_groups_users'] + * ]; */ public $belongsToMany = []; /** * @var array morphTo another record using local key and type attributes * - * protected $morphTo = [ - * 'pictures' => [] - * ]; + * protected $morphTo = [ + * 'pictures' => [] + * ]; */ public $morphTo = []; /** * @var array morphOne related record, inverse of morphTo. * - * protected $morphOne = [ - * 'log' => [History::class, 'name' => 'user'] - * ]; + * protected $morphOne = [ + * 'log' => [History::class, 'name' => 'user'] + * ]; */ public $morphOne = []; /** * @var array morphMany related records, inverse of morphTo. * - * protected $morphMany = [ - * 'log' => [History::class, 'name' => 'user'] - * ]; + * protected $morphMany = [ + * 'log' => [History::class, 'name' => 'user'] + * ]; */ public $morphMany = []; /** * @var array morphToMany to multiple records using a join table. * - * protected $morphToMany = [ - * 'tag' => [Tag::class, 'table' => 'tagables', 'name' => 'tagable'] - * ]; + * protected $morphToMany = [ + * 'tag' => [Tag::class, 'table' => 'tagables', 'name' => 'tagable'] + * ]; */ public $morphToMany = []; /** - * @var array morphedByMany + * @var array morphedByMany to a polymorphic, inverse many-to-many relationship. + * + * public $morphedByMany = [ + * 'tag' => [Tag::class, 'table' => 'tagables', 'name' => 'tagable'] + * ]; */ public $morphedByMany = []; /** * @var array attachOne file attachment. * - * protected $attachOne = [ - * 'picture' => [\October\Rain\Database\Attach\File::class, 'public' => false] - * ]; + * protected $attachOne = [ + * 'picture' => [\October\Rain\Database\Attach\File::class, 'public' => false] + * ]; */ public $attachOne = []; /** * @var array attachMany file attachments. * - * protected $attachMany = [ - * 'pictures' => [\October\Rain\Database\Attach\File::class, 'name'=> 'imageable'] - * ]; + * protected $attachMany = [ + * 'pictures' => [\October\Rain\Database\Attach\File::class, 'name'=> 'imageable'] + * ]; */ public $attachMany = []; /** * @var array hasManyThrough is related records through another record. * - * protected $hasManyThrough = [ - * 'posts' => [Post::class, 'through' => User::class] - * ]; + * protected $hasManyThrough = [ + * 'posts' => [Post::class, 'through' => User::class] + * ]; */ public $hasManyThrough = []; /** * @var array hasOneThrough is a related record through another record. * - * protected $hasOneThrough = [ - * 'post' => [Post::class, 'through' => User::class] - * ]; + * protected $hasOneThrough = [ + * 'post' => [Post::class, 'through' => User::class] + * ]; */ public $hasOneThrough = []; @@ -283,7 +289,8 @@ public function makeRelation($name) } /** - * makeRelationInternal + * makeRelationInternal is used internally to create a new related instance. It also + * fires the `afterRelation` to extend the created instance. */ protected function makeRelationInternal(string $relationName, string $relationClass) { @@ -297,7 +304,7 @@ protected function makeRelationInternal(string $relationName, string $relationCl /** * isRelationPushable determines whether the specified relation should be saved - * when push() is called instead of save() on the model. Default: true. + * when `push()` is called instead of `save()` on the model. Defaults to `true`. */ public function isRelationPushable(string $name): bool { @@ -312,7 +319,7 @@ public function isRelationPushable(string $name): bool /** * getRelationDefaults returns default relation arguments for a given type. - * @param string $type Relation type + * @param string $type * @return array */ protected function getRelationDefaults($type) @@ -510,9 +517,9 @@ public function morphOne($related, $name, $type = null, $id = null, $localKey = } /** - * belongsTo defines an inverse one-to-one or many relationship. - * Overridden from {@link Eloquent\Model} to allow the usage of the intermediary methods to handle the {@link - * $relationsData} array. + * belongsTo defines an inverse one-to-one or many relationship. Overridden from + * \Eloquent\Model to allow the usage of the intermediary methods to handle the + * relationsData array. * @return \October\Rain\Database\Relations\BelongsTo */ public function belongsTo($related, $foreignKey = null, $parentKey = null, $relationName = null) @@ -536,7 +543,8 @@ public function belongsTo($related, $foreignKey = null, $parentKey = null, $rela /** * morphTo defines a polymorphic, inverse one-to-one or many relationship. - * Overridden from {@link Eloquent\Model} to allow the usage of the intermediary methods to handle the relation. + * Overridden from \Eloquent\Model to allow the usage of the intermediary + * methods to handle the relation. * @return \October\Rain\Database\Relations\BelongsTo */ public function morphTo($name = null, $type = null, $id = null, $ownerKey = null)