Browse Source

Add Enum options trait.

mscherer 2 years ago
parent
commit
be141b64e1

+ 15 - 104
docs/Entity/Enum.md

@@ -1,118 +1,29 @@
-# Static Enums
+# Enums
+Since CakePHP 5 you can now use native enums.
+Just spice it a bit with Tools plugin magic, and all good to go.
 
 
-Enum support via trait and `enum()` method.
+Add the `EnumOptionsTrait` to your enums to have `options()` method available in your templates etc.
 
 
-## Intro
-
-There are many cases where an additional model + table + relation would be total overhead. Like those little "status", "level", "type", "color", "category" attributes.
-Often those attributes are implemented as "enums" in SQL - but cake doesn't support them natively. And it should not IMO. You might also want to read [this](http://komlenic.com/244/8-reasons-why-mysqls-enum-data-type-is-evil/) ;)
-
-If there are only a few values to choose from and if they don't change very often, you might want to consider the following approach.
-It is very efficient and easily expandable on code level.
-
-Further advantages
-- can be used from anywhere (model, controller, view, behavior, component, ...)
-- reorder them dynamically per form by just changing the order of the array keys when you call the method.
-- auto-translated right away (i18n without any translation tables - very fast)
-- create multiple static functions for different views ("optionsForAdmins", "optionsForUsers" etc). the overhead is minimal.
-
-
-## Setup
-
-Add tinyint(2) unsigned field called for example "status" (singular).
-"tinyint(2) unsigned" covers 0...127 / 0...255 - which should always be enough for enums. 
-if you need more, you SHOULD make an extra relation as real table. 
-
-Do not use tinyint(1) as CakePHP interprets this as a (boolean) toggle field, which we don't want!
-
-
-## Usage
-
-Add the trait to your entity:
-```php
-use Tools\Model\Entity\EnumTrait;
-
-class MyEntity extends Entity {
-
-    use EnumTrait;
-```
-
-Then add your enums like so:
-
-```php
-    /**
-     * @param int|array|null $value
-     *
-     * @return array|string
-     */
-    public static function statuses($value = null) {
-        $options = [
-            static::STATUS_PENDING => __('Pending'),
-            static::STATUS_SUCCESS => __('Success'),
-            static::STATUS_FAILURE => __('Failure'),
-        ];
-        return parent::enum($value, $options);
-    }
-
-    public const STATUS_PENDING = 0;
-    public const STATUS_SUCCESS = 1;
-    public const STATUS_FAILURE = 2;    
-```
-
-You can now use it in the forms in your templates:
 ```php
 ```php
-<?= $this->Form->create($entity) ?>
-...
-<?= $this->Form->control('status', ['options' => $entity::statuses()]) ?>
-```
+use Tools\Model\Enum\EnumOptionsTrait;
 
 
-And in your index or view:
-```php
-echo $entity::statuses($entity->status);
-```
+enum UserStatus: int implements EnumLabelInterface {
 
 
-Make sure the property is not null (or it would return an array). Best to check for it before or combine 
-it with Shim plugin GetTrait and `$entity->getStatusOrFail()`:
+	use EnumOptionsTrait;
 
 
-```php
-// Allowed to be empty
-echo $entity->status !== null ? $entity::statuses($entity->status) : $default;
+    ...
 
 
-// Required or throw exception
-echo $entity::statuses($entity->getStatusOrFail());
+}
 ```
 ```
 
 
-You can also use it anywhere else for filtering, or comparison:
+If you now need the options array for some entity-less form, you can use it as such:
 ```php
 ```php
-use App\Model\Entity\Notification;
-
-$unreadNotifications = $this->Notifications->find()
-    ->where(['user_id' => $uid, 'status' => Notification::STATUS_UNREAD)])
-    ->all();
+echo $this->Form->control('status', ['options' => \App\Model\Enum\UserStatus::options()]);
 ```
 ```
+The same applies if you ever need to narrow down the options (e.g. not display some values as dropdown option),
+or if you want to resort the options for display:
 
 
-### Subset or custom order
-You can reorder the choices per form by passing a list of keys in the order you want.
-With this, you can also filter the options you want to allow:
 ```php
 ```php
-<?= $this->Form->control('status', [
-    'options' => $entity::statuses([$entity::STATUS_FAILURE, $entity::STATUS_SUCCESS]),
-]) ?>
+$options = UserStatus::options([UserStatus::ACTIVE, UserStatus::INACTIVE]);
+echo $this->Form->control('status', ['options' => $options]);
 ```
 ```
-
-
-
-## Bake template support
-
-Use the [Setup](https://github.com/dereuromark/cakephp-setup) plugin (`--theme Setup`) to 
-get auto-support for your templates based on the existing enums you added.
-
-The above form controls would be auto-added by this.
-
-## Background
-
-See [Static Enums](http://www.dereuromark.de/2010/06/24/static-enums-or-semihardcoded-attributes/).
-
-## Bitmasks
-
-If you are looking for combining several booleans into a single database field check out my [Bitmasked Behavior](http://www.dereuromark.de/2012/02/26/bitmasked-using-bitmasks-in-cakephp/).

+ 118 - 0
docs/Entity/StaticEnum.md

@@ -0,0 +1,118 @@
+# Static Enums
+
+Enum support via trait and `enum()` method.
+
+## Intro
+
+There are many cases where an additional model + table + relation would be total overhead. Like those little "status", "level", "type", "color", "category" attributes.
+Often those attributes are implemented as "enums" in SQL - but cake doesn't support them natively. And it should not IMO. You might also want to read [this](http://komlenic.com/244/8-reasons-why-mysqls-enum-data-type-is-evil/) ;)
+
+If there are only a few values to choose from and if they don't change very often, you might want to consider the following approach.
+It is very efficient and easily expandable on code level.
+
+Further advantages
+- can be used from anywhere (model, controller, view, behavior, component, ...)
+- reorder them dynamically per form by just changing the order of the array keys when you call the method.
+- auto-translated right away (i18n without any translation tables - very fast)
+- create multiple static functions for different views ("optionsForAdmins", "optionsForUsers" etc). the overhead is minimal.
+
+
+## Setup
+
+Add tinyint(2) unsigned field called for example "status" (singular).
+"tinyint(2) unsigned" covers 0...127 / 0...255 - which should always be enough for enums.
+if you need more, you SHOULD make an extra relation as real table.
+
+Do not use tinyint(1) as CakePHP interprets this as a (boolean) toggle field, which we don't want!
+
+
+## Usage
+
+Add the trait to your entity:
+```php
+use Tools\Model\Entity\EnumTrait;
+
+class MyEntity extends Entity {
+
+    use EnumTrait;
+```
+
+Then add your enums like so:
+
+```php
+    /**
+     * @param int|array|null $value
+     *
+     * @return array|string
+     */
+    public static function statuses($value = null) {
+        $options = [
+            static::STATUS_PENDING => __('Pending'),
+            static::STATUS_SUCCESS => __('Success'),
+            static::STATUS_FAILURE => __('Failure'),
+        ];
+        return parent::enum($value, $options);
+    }
+
+    public const STATUS_PENDING = 0;
+    public const STATUS_SUCCESS = 1;
+    public const STATUS_FAILURE = 2;
+```
+
+You can now use it in the forms in your templates:
+```php
+<?= $this->Form->create($entity) ?>
+...
+<?= $this->Form->control('status', ['options' => $entity::statuses()]) ?>
+```
+
+And in your index or view:
+```php
+echo $entity::statuses($entity->status);
+```
+
+Make sure the property is not null (or it would return an array). Best to check for it before or combine
+it with Shim plugin GetTrait and `$entity->getStatusOrFail()`:
+
+```php
+// Allowed to be empty
+echo $entity->status !== null ? $entity::statuses($entity->status) : $default;
+
+// Required or throw exception
+echo $entity::statuses($entity->getStatusOrFail());
+```
+
+You can also use it anywhere else for filtering, or comparison:
+```php
+use App\Model\Entity\Notification;
+
+$unreadNotifications = $this->Notifications->find()
+    ->where(['user_id' => $uid, 'status' => Notification::STATUS_UNREAD)])
+    ->all();
+```
+
+### Subset or custom order
+You can reorder the choices per form by passing a list of keys in the order you want.
+With this, you can also filter the options you want to allow:
+```php
+<?= $this->Form->control('status', [
+    'options' => $entity::statuses([$entity::STATUS_FAILURE, $entity::STATUS_SUCCESS]),
+]) ?>
+```
+
+
+
+## Bake template support
+
+Use the [Setup](https://github.com/dereuromark/cakephp-setup) plugin (`--theme Setup`) to
+get auto-support for your templates based on the existing enums you added.
+
+The above form controls would be auto-added by this.
+
+## Background
+
+See [Static Enums](http://www.dereuromark.de/2010/06/24/static-enums-or-semihardcoded-attributes/).
+
+## Bitmasks
+
+If you are looking for combining several booleans into a single database field check out my [Bitmasked Behavior](http://www.dereuromark.de/2012/02/26/bitmasked-using-bitmasks-in-cakephp/).

+ 19 - 17
docs/README.md

@@ -8,25 +8,25 @@
 
 
 ## Detailed Documentation - Quicklinks
 ## Detailed Documentation - Quicklinks
 
 
-Routing:
+### Routing
 * [Url](Url/Url.md) for useful tooling around URL generation.
 * [Url](Url/Url.md) for useful tooling around URL generation.
 
 
-I18n:
+### I18n
 * [I18n](I18n/I18n.md) for language detection and switching
 * [I18n](I18n/I18n.md) for language detection and switching
 
 
-ErrorHandler
+### ErrorHandler
 * [ErrorHandler](Error/ErrorHandler.md) for improved error handling.
 * [ErrorHandler](Error/ErrorHandler.md) for improved error handling.
 
 
-Email
+### Email
 * [Email](Mailer/Email.md) for sending Emails
 * [Email](Mailer/Email.md) for sending Emails
 
 
-Tokens
+### Tokens
 * [Tokens](Model/Tokens.md) for Token usage
 * [Tokens](Model/Tokens.md) for Token usage
 
 
-Controller:
+### Controller
 * [Controller](Controller/Controller.md)
 * [Controller](Controller/Controller.md)
 
 
-Behaviors:
+### Behaviors
 * [AfterSave](Behavior/AfterSave.md)
 * [AfterSave](Behavior/AfterSave.md)
 * [Jsonable](Behavior/Jsonable.md)
 * [Jsonable](Behavior/Jsonable.md)
 * [Passwordable](Behavior/Passwordable.md)
 * [Passwordable](Behavior/Passwordable.md)
@@ -36,38 +36,40 @@ Behaviors:
 * [String](Behavior/String.md)
 * [String](Behavior/String.md)
 * [Toggle](Behavior/Toggle.md)
 * [Toggle](Behavior/Toggle.md)
 
 
-Components:
+### Components
 * [Common](Component/Common.md)
 * [Common](Component/Common.md)
 * [Mobile](Component/Mobile.md)
 * [Mobile](Component/Mobile.md)
 * [RefererRedirect](Component/RefererRedirect.md)
 * [RefererRedirect](Component/RefererRedirect.md)
 
 
-Helpers:
+### Helpers
 * [Html](Helper/Html.md)
 * [Html](Helper/Html.md)
 * [Form](Helper/Form.md)
 * [Form](Helper/Form.md)
 * [Common](Helper/Common.md)
 * [Common](Helper/Common.md)
 * [Format](Helper/Format.md)
 * [Format](Helper/Format.md)
-* [Icon](Helper/Icon.md)
+* [Icon](Helper/Icon.md) [Deprecated, use Icon plugin instead]
 * [Progress](Helper/Progress.md)
 * [Progress](Helper/Progress.md)
 * [Meter](Helper/Meter.md)
 * [Meter](Helper/Meter.md)
 * [Tree](Helper/Tree.md)
 * [Tree](Helper/Tree.md)
 * [Typography](Helper/Typography.md)
 * [Typography](Helper/Typography.md)
 
 
-Widgets:
+### Widgets
 * [Datalist](Widget/Datalist.md)
 * [Datalist](Widget/Datalist.md)
 
 
-Entity:
-* [Enum](Entity/Enum.md)
+### Model/Entity
+* [Enums](Entity/Enum.md) using native enums (NEW)
+* [StaticEnums](Entity/StaticEnum.md) using static entity methods
+
+Note: Using native enums is recommended since CakePHP 5.
 
 
-Utility:
+### Utility
 * [FileLog](Utility/FileLog.md) to log data into custom file(s) with one line
 * [FileLog](Utility/FileLog.md) to log data into custom file(s) with one line
 
 
-Command:
+### Command
 * [Inflect](Command/Inflect.md) to test inflection of words.
 * [Inflect](Command/Inflect.md) to test inflection of words.
 
 
 ## IDE compatibility improvements
 ## IDE compatibility improvements
 For some methods you can find a IdeHelper task in [IdeHelperExtra plugin](https://github.com/dereuromark/cakephp-ide-helper-extra/):
 For some methods you can find a IdeHelper task in [IdeHelperExtra plugin](https://github.com/dereuromark/cakephp-ide-helper-extra/):
-- `FormatHelper::icon()` (deprecated)
-- `IconHelper::render()`
+- `IconHelper::render()` (deprecated)
 
 
 Those will give you automcomplete for the input.
 Those will give you automcomplete for the input.
 
 

+ 38 - 0
src/Model/Enum/EnumOptionsTrait.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace Tools\Model\Enum;
+
+/**
+ * @mixin \BackedEnum&\Cake\Database\Type\EnumLabelInterface
+ */
+trait EnumOptionsTrait {
+
+	/**
+	 * @param array<int|string|\BackedEnum> $cases Provide for narrowing or resorting.
+	 *
+	 * @return array<string, string>
+	 */
+	public static function options(array $cases = []): array {
+		$options = [];
+
+		if ($cases) {
+			foreach ($cases as $case) {
+				if (!($case instanceof \BackedEnum)) {
+					$case = static::from($case);
+				}
+
+				$options[(string)$case->value] = $case->label();
+			}
+
+			return $options;
+		}
+
+		$cases = static::cases();
+		foreach ($cases as $case) {
+			$options[(string)$case->value] = $case->label();
+		}
+
+		return $options;
+	}
+
+}

+ 40 - 0
tests/TestCase/Model/Enum/EnumOptionsTraitTest.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Tools\Test\TestCase\Model\Enum;
+
+use Shim\TestSuite\TestCase;
+use TestApp\Model\Enum\FooBar;
+
+class EnumOptionsTraitTest extends TestCase {
+
+	/**
+	 * Test partial options.
+	 *
+	 * @return void
+	 */
+	public function testEnumNarrowing() {
+		$array = [
+			1 => 'One',
+			2 => 'Two',
+		];
+
+		$res = FooBar::options([1, 2]);
+		$expected = $array;
+		$this->assertSame($expected, $res);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testEnumResorting() {
+		$array = [
+			2 => 'Two',
+			1 => 'One',
+		];
+
+		$res = FooBar::options([FooBar::TWO, FooBar::ONE], $array);
+		$expected = $array;
+		$this->assertSame($expected, $res);
+	}
+
+}

+ 23 - 0
tests/test_app/Model/Enum/FooBar.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace TestApp\Model\Enum;
+
+use Cake\Database\Type\EnumLabelInterface;
+use Cake\Utility\Inflector;
+use Tools\Model\Enum\EnumOptionsTrait;
+
+enum FooBar: int implements EnumLabelInterface
+{
+	use EnumOptionsTrait;
+
+	case ZERO = 0;
+	case ONE = 1;
+	case TWO = 2;
+
+	/**
+	 * @return string
+	 */
+	public function label(): string {
+		return Inflector::humanize(mb_strtolower($this->name));
+	}
+}