├── .codeclimate.yml ├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── extension.neon ├── phpstan.neon └── src ├── Composer └── IdeHelper.php ├── Concerns ├── Attributes.php ├── Bitmasks.php ├── Comparison.php ├── Configure.php ├── ConfigureDefaults.php ├── ConfigureLabels.php ├── ConfigureMapper.php ├── ConfigureState.php ├── Constructor.php ├── Defaults.php ├── Dropdown.php ├── Enhancers.php ├── Extractor.php ├── From.php ├── Getters.php ├── Labels.php ├── Macros.php ├── MagicCalls.php ├── Mappers.php ├── Properties.php ├── Reporters.php ├── State.php ├── Subset.php └── Value.php ├── Contracts ├── EnumSubset.php ├── Mapper.php ├── Reporter.php └── TransitionHook.php ├── Enums └── LogLevel.php ├── Exceptions ├── EnumException.php ├── IllegalEnumTransitionException.php ├── InvalidBitmaskEnum.php ├── NotAnEnumException.php ├── PropertyAlreadyStoredException.php ├── ReservedPropertyNameException.php └── SyntaxException.php ├── Functions └── Functions.php ├── Helpers ├── Bitmasks │ ├── Bitmask.php │ ├── Concerns │ │ ├── BitmaskModifiers.php │ │ └── BitmaskValidators.php │ └── EnumBitmasks.php ├── EnumAttributes.php ├── EnumBlade.php ├── EnumCheck.php ├── EnumCompare.php ├── EnumDefaults.php ├── EnumExtractor.php ├── EnumGetters.php ├── EnumImplements.php ├── EnumLabels.php ├── EnumMacros.php ├── EnumMagicCalls.php ├── EnumProperties.php ├── EnumProxy.php ├── EnumReporter.php ├── EnumState.php ├── EnumValue.php ├── Enumhancer.php ├── Mappers │ ├── EnumArrayMapper.php │ └── EnumMapper.php └── Subset │ └── EnumSubsetMethods.php ├── Laravel ├── Concerns │ ├── CastsBasicEnumerations.php │ └── CastsStatefulEnumerations.php ├── Middleware │ └── SubstituteEnums.php ├── Mixins │ ├── FormRequestMixin.php │ └── RulesMixin.php ├── Providers │ └── EnumhancerServiceProvider.php ├── Reporters │ └── LaravelLogReporter.php └── Rules │ ├── EnumBitmask.php │ ├── EnumTransition.php │ └── IsEnum.php └── PHPStan ├── Constants ├── BitmaskConstantAlwaysUsed.php ├── BitmaskModifierConstantAlwaysUsed.php ├── DefaultConstantAlwaysUsed.php ├── MapperConstantAlwaysUsed.php ├── Rules │ ├── DefaultConstantRule.php │ ├── MapperConstantRule.php │ └── StrictConstantRule.php └── StrictConstantAlwaysUsed.php ├── Methods ├── EnumComparisonMethodsClassReflection.php ├── EnumConstructorMethodsClassReflection.php ├── EnumMacrosMethodsClassReflection.php └── EnumStateMethodsClassReflection.php └── Reflections └── ClosureMethodReflection.php /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | plugins: 3 | markdownlint: 4 | exclude_patterns: 5 | - vendor 6 | - LICENSE.md 7 | enabled: true 8 | checks: 9 | MD029: 10 | enabled: false 11 | phpcodesniffer: 12 | channel: "beta" 13 | config: 14 | standard: "PSR1,PSR2" 15 | exclude_patterns: 16 | - "**/*" 17 | - "!src/*" 18 | - 'src/Enums/*' 19 | enabled: true 20 | phpmd: 21 | enabled: true 22 | exclude_patterns: 23 | - !**/*.php 24 | checks: 25 | CleanCode/StaticAccess: 26 | enabled: false 27 | CleanCode/BooleanArgumentFlag: 28 | enabled: false 29 | Naming/ShortMethodName: 30 | enabled: false 31 | duplication: 32 | enabled: true 33 | exclude_patterns: 34 | - config/ 35 | - 'coverage' 36 | - .github 37 | - db/ 38 | - dist/ 39 | - features/ 40 | - '**/node_modules/' 41 | - script/ 42 | - '**/spec/' 43 | - '**/test/' 44 | - '**/tests/' 45 | - Tests/ 46 | - '**/vendor/' 47 | - '**/*_test.go' 48 | - '**/*.d.ts' 49 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `Enumhancer` will be documented in this file 4 | 5 | ## 2.2.0 - 2023-06-09 6 | 7 | - fixed serious bug in [Getters](docs/getters.md) where 8 | getting by integer would not match value first. 9 | - added support for [BIT_MODIFIER](docs/bitmasks.md#modifiers) 10 | 11 | ## 2.1.0 - 2023-05-12 12 | 13 | - Support for [Attributes](docs/attributes.md) 14 | 15 | ## 2.0.0 - 2023-02-28 16 | 17 | - Now supports Laravel 10 18 | - dropped support for laravel 8 19 | 20 | ### Upgrade notes 21 | 22 | - Makers (make, tryMake etc.) are removed in favor 23 | of [Getters](docs/getters.md) 24 | 25 | ## 1.23.0 - 2023-02-03 26 | 27 | - added [PHPStan](docs/phpstan.md) support 28 | - added [IDE-helper](docs/ide-helper.md) (requires another package) 29 | - added global class for configuring Enumhancer 30 | - added global [Macro](docs/macros.md#global-macros) support 31 | 32 | ## 1.22.0 - 2023-01-07 33 | 34 | - added [asEnum](docs/formrequests.md) to laravel's FormRequests 35 | - tiny fix in [isEnum](docs/laravel.validation.md#isEnum) 36 | validation: When [Defaults](docs/defaults.md) are used, it 37 | should fail validation. 38 | 39 | ## 1.21.0 - 2023-01-06 40 | 41 | - added [(basic) enum binding](docs/binding.md) allowing you to bind 42 | basic enumerations to your routes and use Enumhancers secret sauce. 43 | - Fixed a lot of potential issues with PHPstan. 44 | 45 | ## 1.20.0 - 2023-01-04 46 | 47 | - bugfix in [Default](docs/defaults.md) where configured defaults would 48 | not override the by const defined value 49 | - bugfix in [Mappers](docs/mappers.md) where mapping to integers was 50 | not allowed 51 | 52 | ### Extended features 53 | 54 | - You can now set Mapper FQCN in constants starting with 55 | `map` and `map_flip` 56 | - [Mappers](docs/mappers.md) methods now are usable statically 57 | - All Laravel rules have now macro's set on `Rule` 58 | 59 | ### New features 60 | 61 | - added [Bitmask](docs/bitmasks.md) 62 | - added [Macros](docs/macros.md) 63 | - added `isEnum` and `enumBitmask` rules 64 | 65 | ## 1.19.0 - 2022-12-15 66 | 67 | - You can now use constants for [Mappers](docs/mappers.md) 68 | and [Defaults](docs/defaults.md) 69 | - you can now flag a unit enum as `strict`, so you don't 70 | have to worry about casing in [Values](docs/value.md). 71 | 72 | ## 1.18.0 - 2022-12-14 73 | 74 | - Added Magic method functionality to [State](docs/state.md) 75 | - Added `to` and `tryTo` methods to `State` 76 | - Added `is`, `isNot`, `isIn` and `isNotIn` 77 | to [Comparison](docs/comparison.md) 78 | 79 | ## 1.17.0 - 2022-12-13 80 | 81 | - Added [Flip](docs/mappers.md#flip), allowing to use 82 | a single mapper for mapping between enums 83 | - [From](docs/from.md) 84 | now allows `UnitEnum` objects for use with `Flip` 85 | - [Comparison](docs/comparison.md) now allows different enums 86 | when used with [Mappers](docs/mappers.md) 87 | - Deprecated [Makers](docs/makers.md), replaced by 88 | [Getters](docs/getters.md) 89 | 90 | ## 1.16.0 - 2022-12-11 91 | 92 | - Added [Configure](docs/configure.md) 93 | - Added [Dropdown](docs/dropdown.md) 94 | - [Comparison](docs/configure.md) now accepts null values 95 | - Fixed bug in [Casting](docs/casting.md) where in the latest Laravel versions 96 | the `Keep Enum Value Case` switch no longer worked. 97 | 98 | ## 1.15.0 - 2022-06-21 99 | 100 | - Made the Laravel [Reporter](docs/reporters.md#laravel) configurable 101 | - added `key` method to [Value](docs/value.md) 102 | 103 | ## 1.14.0 - 2022-06-19 104 | 105 | - Added transition hooks [State](docs/state.md) 106 | - [Makers](docs/makers.md) & [From](docs/from.md) now allow you to use integer 107 | keys on basic and string enums 108 | 109 | ## 1.12.0 - 2022-06-15 110 | 111 | - Added casting support for [State](docs/state.md) 112 | 113 | ## 1.11.0 - 2022-06-14 114 | 115 | - Added [State](docs/state.md) that allows you to have transitions with enums 116 | 117 | ## 1.10.0 - 2022-06-12 118 | 119 | - Added [Defaults](docs/defaults.md) that allows you to have default enums 120 | 121 | ## 1.9.0 - 2022-06-08 122 | 123 | - Added [Blade](docs/blade.md) support 124 | 125 | ## 1.8.0 - 2022-06-07 126 | 127 | - Added [Helper functions](docs/functions.md) to ease usage of basic enums 128 | 129 | ## 1.7.0 - 2022-06-06 130 | 131 | - When using [Comparison](docs/comparison.md), you can now assert with `is` 132 | or `isNot` 133 | 134 | ## 1.6.0 - 2022-06-04 135 | 136 | - Added Eloquent Casting support for basic enumerations 137 | 138 | ## 1.5.0 - 2022-05-31 139 | 140 | - Added [Extractor](docs/extractor.md) to extract enums from a string mentioned 141 | by value 142 | - Some documentation repairs 143 | 144 | ## 1.4.1 - 2022-03-04 145 | 146 | - Added `cases` method to `Subset` 147 | 148 | ## 1.4.0 - 2022-03-02 149 | 150 | - Renamed Multi to Subset 151 | - Added `names` method to `Subset` 152 | - Added `values` method to `Subset` 153 | - Added `do` method to `Subset` 154 | 155 | ## 1.3.0 - 2022-02-28 156 | 157 | - Added Multi. Currently allows you to compare against a subset of your enum 158 | 159 | ## 1.2.0 - 2022-02-26 160 | 161 | - Added Value (for use with basic enums) 162 | 163 | ## 1.1.0 - 2022-02-25 164 | 165 | - Added From. Useful for situations where you need them with basic enums 166 | 167 | ## 1.0.2 - 2022-02-16 168 | 169 | - Bugfix: Constructor did not use internal mapper 170 | 171 | ## 1.0.1 - 2022-02-16 172 | 173 | - You can now define a mapper in a method 174 | - When you use an empty string or null in mappable, it will return null now 175 | 176 | ## 1.0.0 - 2022-02-15 177 | 178 | - Initial release 179 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or 6 | pull request. 7 | 8 | ## Etiquette 9 | 10 | This project is open source, and as such, the maintainers give their free time 11 | to build and maintain the source code held within. They make the code freely 12 | available in the hope that it will be of use to other developers. It would be 13 | extremely unfair for them to suffer abuse or anger for their hard work. 14 | 15 | Please be considerate towards maintainers when raising issues or presenting pull 16 | requests. Let's show the world that developers are civilized and selfless 17 | people. 18 | 19 | It's the duty of the maintainer to ensure that all submissions to the project 20 | are of sufficient quality to benefit the project. Many developers have different 21 | skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do 22 | not be upset or abusive if your submission is not used. 23 | 24 | ## Viability 25 | 26 | When requesting or submitting new features, first consider whether it might be 27 | useful to others. Open source projects are used by many developers, who may have 28 | entirely different needs to your own. Think about whether or not your feature is 29 | likely to be used by other users of the project. 30 | 31 | ## Procedure 32 | 33 | Before filing an issue: 34 | 35 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental 36 | incident. 37 | - Check to make sure your feature suggestion isn't already present within the 38 | project. 39 | - Check the pull requests tab to ensure that the bug doesn't have a fix in 40 | progress. 41 | - Check the pull requests tab to ensure that the feature isn't already in 42 | progress. 43 | 44 | Before submitting a pull request: 45 | 46 | - Check the codebase to ensure that your feature doesn't already exist. 47 | - Check the pull requests to ensure that another person hasn't already submitted 48 | the feature or fix. 49 | 50 | ## Requirements 51 | 52 | If the project maintainer has any additional requirements, you will find them 53 | listed here. 54 | 55 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** 56 | - The easiest way to apply the conventions is to 57 | install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 58 | 59 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 60 | 61 | - **Document any change in behaviour** - Make sure the `README.md` and any other 62 | relevant documentation are kept up-to-date. 63 | 64 | - **Consider our release cycle** - We try to 65 | follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is 66 | not an option. 67 | 68 | - **One pull request per feature** - If you want to do more than one thing, send 69 | multiple pull requests. 70 | 71 | - **Send coherent history** - Make sure each individual commit in your pull 72 | request is meaningful. If you had to make multiple intermediate commits while 73 | developing, 74 | please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) 75 | before submitting. 76 | 77 | **Happy coding**! 78 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies of this license 6 | document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for software 11 | and other kinds of works, specifically designed to ensure cooperation with the 12 | community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed to take 15 | away your freedom to share and change the works. By contrast, our General Public 16 | Licenses are intended to guarantee your freedom to share and change all versions 17 | of a program--to make sure it remains free software for all its users. 18 | 19 | When we speak of free software, we are referring to freedom, not price. Our 20 | General Public Licenses are designed to make sure that you have the freedom to 21 | distribute copies of free software (and charge for them if you wish), that you 22 | receive source code or can get it if you want it, that you can change the 23 | software or use pieces of it in new free programs, and that you know you can do 24 | these things. 25 | 26 | Developers that use our General Public Licenses protect your rights with two 27 | steps: (1) assert copyright on the software, and (2) offer you this License 28 | which gives you legal permission to copy, distribute and/or modify the software. 29 | 30 | A secondary benefit of defending all users' freedom is that improvements made in 31 | alternate versions of the program, if they receive widespread use, become 32 | available for other developers to incorporate. Many developers of free software 33 | are heartened and encouraged by the resulting cooperation. However, in the case 34 | of software used on network servers, this result may fail to come about. The GNU 35 | General Public License permits making a modified version and letting the public 36 | access it on a server without ever releasing its source code to the public. 37 | 38 | The GNU Affero General Public License is designed specifically to ensure that, 39 | in such cases, the modified source code becomes available to the community. It 40 | requires the operator of a network server to provide the source code of the 41 | modified version running there to the users of that server. Therefore, public 42 | use of a modified version, on a publicly accessible server, gives the public 43 | access to the source code of the modified version. 44 | 45 | An older license, called the Affero General Public License and published by 46 | Affero, was designed to accomplish similar goals. This is a different license, 47 | not a version of the Affero GPL, but Affero has released a new version of the 48 | Affero GPL which permits relicensing under this license. 49 | 50 | The precise terms and conditions for copying, distribution and modification 51 | follow. 52 | 53 | TERMS AND CONDITIONS 54 | 55 | 0. Definitions. 56 | 57 | "This License" refers to version 3 of the GNU Affero General Public License. 58 | 59 | "Copyright" also means copyright-like laws that apply to other kinds of works, 60 | such as semiconductor masks. 61 | 62 | "The Program" refers to any copyrightable work licensed under this License. Each 63 | licensee is addressed as "you". "Licensees" and 64 | "recipients" may be individuals or organizations. 65 | 66 | To "modify" a work means to copy from or adapt all or part of the work in a 67 | fashion requiring copyright permission, other than the making of an exact copy. 68 | The resulting work is called a "modified version" of the earlier work or a 69 | work "based on" the earlier work. 70 | 71 | A "covered work" means either the unmodified Program or a work based on the 72 | Program. 73 | 74 | To "propagate" a work means to do anything with it that, without permission, 75 | would make you directly or secondarily liable for infringement under applicable 76 | copyright law, except executing it on a computer or modifying a private copy. 77 | Propagation includes copying, distribution (with or without modification), 78 | making available to the public, and in some countries other activities as well. 79 | 80 | To "convey" a work means any kind of propagation that enables other parties to 81 | make or receive copies. Mere interaction with a user through a computer network, 82 | with no transfer of a copy, is not conveying. 83 | 84 | An interactive user interface displays "Appropriate Legal Notices" 85 | to the extent that it includes a convenient and prominently visible feature 86 | that (1) displays an appropriate copyright notice, and (2) 87 | tells the user that there is no warranty for the work (except to the extent that 88 | warranties are provided), that licensees may convey the work under this License, 89 | and how to view a copy of this License. If the interface presents a list of user 90 | commands or options, such as a menu, a prominent item in the list meets this 91 | criterion. 92 | 93 | 1. Source Code. 94 | 95 | The "source code" for a work means the preferred form of the work for making 96 | modifications to it. "Object code" means any non-source form of a work. 97 | 98 | A "Standard Interface" means an interface that either is an official standard 99 | defined by a recognized standards body, or, in the case of interfaces specified 100 | for a particular programming language, one that is widely used among developers 101 | working in that language. 102 | 103 | The "System Libraries" of an executable work include anything, other than the 104 | work as a whole, that (a) is included in the normal form of packaging a Major 105 | Component, but which is not part of that Major Component, and (b) serves only to 106 | enable use of the work with that Major Component, or to implement a Standard 107 | Interface for which an implementation is available to the public in source code 108 | form. A 109 | "Major Component", in this context, means a major essential component 110 | (kernel, window system, and so on) of the specific operating system 111 | (if any) on which the executable work runs, or a compiler used to produce the 112 | work, or an object code interpreter used to run it. 113 | 114 | The "Corresponding Source" for a work in object code form means all the source 115 | code needed to generate, install, and (for an executable work) run the object 116 | code and to modify the work, including scripts to control those activities. 117 | However, it does not include the work's System Libraries, or general-purpose 118 | tools or generally available free programs which are used unmodified in 119 | performing those activities but which are not part of the work. For example, 120 | Corresponding Source includes interface definition files associated with source 121 | files for the work, and the source code for shared libraries and dynamically 122 | linked subprograms that the work is specifically designed to require, such as by 123 | intimate data communication or control flow between those subprograms and other 124 | parts of the work. 125 | 126 | The Corresponding Source need not include anything that users can regenerate 127 | automatically from other parts of the Corresponding Source. 128 | 129 | The Corresponding Source for a work in source code form is that same work. 130 | 131 | 2. Basic Permissions. 132 | 133 | All rights granted under this License are granted for the term of copyright on 134 | the Program, and are irrevocable provided the stated conditions are met. This 135 | License explicitly affirms your unlimited permission to run the unmodified 136 | Program. The output from running a covered work is covered by this License only 137 | if the output, given its content, constitutes a covered work. This License 138 | acknowledges your rights of fair use or other equivalent, as provided by 139 | copyright law. 140 | 141 | You may make, run and propagate covered works that you do not convey, without 142 | conditions so long as your license otherwise remains in force. You may convey 143 | covered works to others for the sole purpose of having them make modifications 144 | exclusively for you, or provide you with facilities for running those works, 145 | provided that you comply with the terms of this License in conveying all 146 | material for which you do not control copyright. Those thus making or running 147 | the covered works for you must do so exclusively on your behalf, under your 148 | direction and control, on terms that prohibit them from making any copies of 149 | your copyrighted material outside their relationship with you. 150 | 151 | Conveying under any other circumstances is permitted solely under the conditions 152 | stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 153 | 154 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 155 | 156 | No covered work shall be deemed part of an effective technological measure under 157 | any applicable law fulfilling obligations under article 11 of the WIPO copyright 158 | treaty adopted on 20 December 1996, or similar laws prohibiting or restricting 159 | circumvention of such measures. 160 | 161 | When you convey a covered work, you waive any legal power to forbid 162 | circumvention of technological measures to the extent such circumvention is 163 | effected by exercising rights under this License with respect to the covered 164 | work, and you disclaim any intention to limit operation or modification of the 165 | work as a means of enforcing, against the work's users, your or third parties' 166 | legal rights to forbid circumvention of technological measures. 167 | 168 | 4. Conveying Verbatim Copies. 169 | 170 | You may convey verbatim copies of the Program's source code as you receive it, 171 | in any medium, provided that you conspicuously and appropriately publish on each 172 | copy an appropriate copyright notice; keep intact all notices stating that this 173 | License and any non-permissive terms added in accord with section 7 apply to the 174 | code; keep intact all notices of the absence of any warranty; and give all 175 | recipients a copy of this License along with the Program. 176 | 177 | You may charge any price or no price for each copy that you convey, and you may 178 | offer support or warranty protection for a fee. 179 | 180 | 5. Conveying Modified Source Versions. 181 | 182 | You may convey a work based on the Program, or the modifications to produce it 183 | from the Program, in the form of source code under the terms of section 4, 184 | provided that you also meet all of these conditions: 185 | 186 | a) The work must carry prominent notices stating that you modified 187 | it, and giving a relevant date. 188 | 189 | b) The work must carry prominent notices stating that it is 190 | released under this License and any conditions added under section 191 | 7. This requirement modifies the requirement in section 4 to 192 | "keep intact all notices". 193 | 194 | c) You must license the entire work, as a whole, under this 195 | License to anyone who comes into possession of a copy. This 196 | License will therefore apply, along with any applicable section 7 197 | additional terms, to the whole of the work, and all its parts, 198 | regardless of how they are packaged. This License gives no 199 | permission to license the work in any other way, but it does not 200 | invalidate such permission if you have separately received it. 201 | 202 | d) If the work has interactive user interfaces, each must display 203 | Appropriate Legal Notices; however, if the Program has interactive 204 | interfaces that do not display Appropriate Legal Notices, your 205 | work need not make them do so. 206 | 207 | A compilation of a covered work with other separate and independent works, which 208 | are not by their nature extensions of the covered work, and which are not 209 | combined with it such as to form a larger program, in or on a volume of a 210 | storage or distribution medium, is called an 211 | "aggregate" if the compilation and its resulting copyright are not used to limit 212 | the access or legal rights of the compilation's users beyond what the individual 213 | works permit. Inclusion of a covered work in an aggregate does not cause this 214 | License to apply to the other parts of the aggregate. 215 | 216 | 6. Conveying Non-Source Forms. 217 | 218 | You may convey a covered work in object code form under the terms of sections 4 219 | and 5, provided that you also convey the machine-readable Corresponding Source 220 | under the terms of this License, in one of these ways: 221 | 222 | a) Convey the object code in, or embodied in, a physical product 223 | (including a physical distribution medium), accompanied by the 224 | Corresponding Source fixed on a durable physical medium 225 | customarily used for software interchange. 226 | 227 | b) Convey the object code in, or embodied in, a physical product 228 | (including a physical distribution medium), accompanied by a 229 | written offer, valid for at least three years and valid for as 230 | long as you offer spare parts or customer support for that product 231 | model, to give anyone who possesses the object code either (1) a 232 | copy of the Corresponding Source for all the software in the 233 | product that is covered by this License, on a durable physical 234 | medium customarily used for software interchange, for a price no 235 | more than your reasonable cost of physically performing this 236 | conveying of source, or (2) access to copy the 237 | Corresponding Source from a network server at no charge. 238 | 239 | c) Convey individual copies of the object code with a copy of the 240 | written offer to provide the Corresponding Source. This 241 | alternative is allowed only occasionally and noncommercially, and 242 | only if you received the object code with such an offer, in accord 243 | with subsection 6b. 244 | 245 | d) Convey the object code by offering access from a designated 246 | place (gratis or for a charge), and offer equivalent access to the 247 | Corresponding Source in the same way through the same place at no 248 | further charge. You need not require recipients to copy the 249 | Corresponding Source along with the object code. If the place to 250 | copy the object code is a network server, the Corresponding Source 251 | may be on a different server (operated by you or a third party) 252 | that supports equivalent copying facilities, provided you maintain 253 | clear directions next to the object code saying where to find the 254 | Corresponding Source. Regardless of what server hosts the 255 | Corresponding Source, you remain obligated to ensure that it is 256 | available for as long as needed to satisfy these requirements. 257 | 258 | e) Convey the object code using peer-to-peer transmission, provided 259 | you inform other peers where the object code and Corresponding 260 | Source of the work are being offered to the general public at no 261 | charge under subsection 6d. 262 | 263 | A separable portion of the object code, whose source code is excluded from the 264 | Corresponding Source as a System Library, need not be included in conveying the 265 | object code work. 266 | 267 | A "User Product" is either (1) a "consumer product", which means any tangible 268 | personal property which is normally used for personal, family, or household 269 | purposes, or (2) anything designed or sold for incorporation into a dwelling. In 270 | determining whether a product is a consumer product, doubtful cases shall be 271 | resolved in favor of coverage. For a particular product received by a particular 272 | user, "normally used" refers to a typical or common use of that class of 273 | product, regardless of the status of the particular user or of the way in which 274 | the particular user actually uses, or expects or is expected to use, the 275 | product. A product is a consumer product regardless of whether the product has 276 | substantial commercial, industrial or non-consumer uses, unless such uses 277 | represent the only significant mode of use of the product. 278 | 279 | "Installation Information" for a User Product means any methods, procedures, 280 | authorization keys, or other information required to install and execute 281 | modified versions of a covered work in that User Product from a modified version 282 | of its Corresponding Source. The information must suffice to ensure that the 283 | continued functioning of the modified object code is in no case prevented or 284 | interfered with solely because modification has been made. 285 | 286 | If you convey an object code work under this section in, or with, or 287 | specifically for use in, a User Product, and the conveying occurs as part of a 288 | transaction in which the right of possession and use of the User Product is 289 | transferred to the recipient in perpetuity or for a fixed term (regardless of 290 | how the transaction is characterized), the Corresponding Source conveyed under 291 | this section must be accompanied by the Installation Information. But this 292 | requirement does not apply if neither you nor any third party retains the 293 | ability to install modified object code on the User Product (for example, the 294 | work has been installed in ROM). 295 | 296 | The requirement to provide Installation Information does not include a 297 | requirement to continue to provide support service, warranty, or updates for a 298 | work that has been modified or installed by the recipient, or for the User 299 | Product in which it has been modified or installed. Access to a network may be 300 | denied when the modification itself materially and adversely affects the 301 | operation of the network or violates the rules and protocols for communication 302 | across the network. 303 | 304 | Corresponding Source conveyed, and Installation Information provided, in accord 305 | with this section must be in a format that is publicly documented (and with an 306 | implementation available to the public in source code form), and must require no 307 | special password or key for unpacking, reading or copying. 308 | 309 | 7. Additional Terms. 310 | 311 | "Additional permissions" are terms that supplement the terms of this License by 312 | making exceptions from one or more of its conditions. Additional permissions 313 | that are applicable to the entire Program shall be treated as though they were 314 | included in this License, to the extent that they are valid under applicable 315 | law. If additional permissions apply only to part of the Program, that part may 316 | be used separately under those permissions, but the entire Program remains 317 | governed by this License without regard to the additional permissions. 318 | 319 | When you convey a copy of a covered work, you may at your option remove any 320 | additional permissions from that copy, or from any part of it. (Additional 321 | permissions may be written to require their own removal in certain cases when 322 | you modify the work.) You may place additional permissions on material, added 323 | by you to a covered work, for which you have or can give appropriate copyright 324 | permission. 325 | 326 | Notwithstanding any other provision of this License, for material you add to a 327 | covered work, you may (if authorized by the copyright holders of that material) 328 | supplement the terms of this License with terms: 329 | 330 | a) Disclaiming warranty or limiting liability differently from the 331 | terms of sections 15 and 16 of this License; or 332 | 333 | b) Requiring preservation of specified reasonable legal notices or 334 | author attributions in that material or in the Appropriate Legal 335 | Notices displayed by works containing it; or 336 | 337 | c) Prohibiting misrepresentation of the origin of that material, or 338 | requiring that modified versions of such material be marked in 339 | reasonable ways as different from the original version; or 340 | 341 | d) Limiting the use for publicity purposes of names of licensors or 342 | authors of the material; or 343 | 344 | e) Declining to grant rights under trademark law for use of some 345 | trade names, trademarks, or service marks; or 346 | 347 | f) Requiring indemnification of licensors and authors of that 348 | material by anyone who conveys the material (or modified versions of 349 | it) with contractual assumptions of liability to the recipient, for 350 | any liability that these contractual assumptions directly impose on 351 | those licensors and authors. 352 | 353 | All other non-permissive additional terms are considered "further restrictions" 354 | within the meaning of section 10. If the Program as you received it, or any part 355 | of it, contains a notice stating that it is governed by this License along with 356 | a term that is a further restriction, you may remove that term. If a license 357 | document contains a further restriction but permits relicensing or conveying 358 | under this License, you may add to a covered work material governed by the terms 359 | of that license document, provided that the further restriction does not survive 360 | such relicensing or conveying. 361 | 362 | If you add terms to a covered work in accord with this section, you must place, 363 | in the relevant source files, a statement of the additional terms that apply to 364 | those files, or a notice indicating where to find the applicable terms. 365 | 366 | Additional terms, permissive or non-permissive, may be stated in the form of a 367 | separately written license, or stated as exceptions; the above requirements 368 | apply either way. 369 | 370 | 8. Termination. 371 | 372 | You may not propagate or modify a covered work except as expressly provided 373 | under this License. Any attempt otherwise to propagate or modify it is void, and 374 | will automatically terminate your rights under this License (including any 375 | patent licenses granted under the third paragraph of section 11). 376 | 377 | However, if you cease all violation of this License, then your license from a 378 | particular copyright holder is reinstated (a) 379 | provisionally, unless and until the copyright holder explicitly and finally 380 | terminates your license, and (b) permanently, if the copyright holder fails to 381 | notify you of the violation by some reasonable means prior to 60 days after the 382 | cessation. 383 | 384 | Moreover, your license from a particular copyright holder is reinstated 385 | permanently if the copyright holder notifies you of the violation by some 386 | reasonable means, this is the first time you have received notice of violation 387 | of this License (for any work) from that copyright holder, and you cure the 388 | violation prior to 30 days after your receipt of the notice. 389 | 390 | Termination of your rights under this section does not terminate the licenses of 391 | parties who have received copies or rights from you under this License. If your 392 | rights have been terminated and not permanently reinstated, you do not qualify 393 | to receive new licenses for the same material under section 10. 394 | 395 | 9. Acceptance Not Required for Having Copies. 396 | 397 | You are not required to accept this License in order to receive or run a copy of 398 | the Program. Ancillary propagation of a covered work occurring solely as a 399 | consequence of using peer-to-peer transmission to receive a copy likewise does 400 | not require acceptance. However, nothing other than this License grants you 401 | permission to propagate or modify any covered work. These actions infringe 402 | copyright if you do not accept this License. Therefore, by modifying or 403 | propagating a covered work, you indicate your acceptance of this License to do 404 | so. 405 | 406 | 10. Automatic Licensing of Downstream Recipients. 407 | 408 | Each time you convey a covered work, the recipient automatically receives a 409 | license from the original licensors, to run, modify and propagate that work, 410 | subject to this License. You are not responsible for enforcing compliance by 411 | third parties with this License. 412 | 413 | An "entity transaction" is a transaction transferring control of an 414 | organization, or substantially all assets of one, or subdividing an 415 | organization, or merging organizations. If propagation of a covered work results 416 | from an entity transaction, each party to that transaction who receives a copy 417 | of the work also receives whatever licenses to the work the party's predecessor 418 | in interest had or could give under the previous paragraph, plus a right to 419 | possession of the Corresponding Source of the work from the predecessor in 420 | interest, if the predecessor has it or can get it with reasonable efforts. 421 | 422 | You may not impose any further restrictions on the exercise of the rights 423 | granted or affirmed under this License. For example, you may not impose a 424 | license fee, royalty, or other charge for exercise of rights granted under this 425 | License, and you may not initiate litigation 426 | (including a cross-claim or counterclaim in a lawsuit) alleging that any patent 427 | claim is infringed by making, using, selling, offering for sale, or importing 428 | the Program or any portion of it. 429 | 430 | 11. Patents. 431 | 432 | A "contributor" is a copyright holder who authorizes use under this License of 433 | the Program or a work on which the Program is based. The work thus licensed is 434 | called the contributor's "contributor version". 435 | 436 | A contributor's "essential patent claims" are all patent claims owned or 437 | controlled by the contributor, whether already acquired or hereafter acquired, 438 | that would be infringed by some manner, permitted by this License, of making, 439 | using, or selling its contributor version, but do not include claims that would 440 | be infringed only as a consequence of further modification of the contributor 441 | version. For purposes of this definition, "control" includes the right to grant 442 | patent sublicenses in a manner consistent with the requirements of this License. 443 | 444 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent 445 | license under the contributor's essential patent claims, to make, use, sell, 446 | offer for sale, import and otherwise run, modify and propagate the contents of 447 | its contributor version. 448 | 449 | In the following three paragraphs, a "patent license" is any express agreement 450 | or commitment, however denominated, not to enforce a patent 451 | (such as an express permission to practice a patent or covenant not to sue for 452 | patent infringement). To "grant" such a patent license to a party means to make 453 | such an agreement or commitment not to enforce a patent against the party. 454 | 455 | If you convey a covered work, knowingly relying on a patent license, and the 456 | Corresponding Source of the work is not available for anyone to copy, free of 457 | charge and under the terms of this License, through a publicly available network 458 | server or other readily accessible means, then you must either (1) cause the 459 | Corresponding Source to be so available, or (2) arrange to deprive yourself of 460 | the benefit of the patent license for this particular work, or (3) arrange, in a 461 | manner consistent with the requirements of this License, to extend the patent 462 | license to downstream recipients. "Knowingly relying" means you have actual 463 | knowledge that, but for the patent license, your conveying the covered work in a 464 | country, or your recipient's use of the covered work in a country, would 465 | infringe one or more identifiable patents in that country that you have reason 466 | to believe are valid. 467 | 468 | If, pursuant to or in connection with a single transaction or arrangement, you 469 | convey, or propagate by procuring conveyance of, a covered work, and grant a 470 | patent license to some of the parties receiving the covered work authorizing 471 | them to use, propagate, modify or convey a specific copy of the covered work, 472 | then the patent license you grant is automatically extended to all recipients of 473 | the covered work and works based on it. 474 | 475 | A patent license is "discriminatory" if it does not include within the scope of 476 | its coverage, prohibits the exercise of, or is conditioned on the non-exercise 477 | of one or more of the rights that are specifically granted under this License. 478 | You may not convey a covered work if you are a party to an arrangement with a 479 | third party that is in the business of distributing software, under which you 480 | make payment to the third party based on the extent of your activity of 481 | conveying the work, and under which the third party grants, to any of the 482 | parties who would receive the covered work from you, a discriminatory patent 483 | license (a) in connection with copies of the covered work conveyed by you (or 484 | copies made from those copies), or (b) primarily for and in connection with 485 | specific products or compilations that contain the covered work, unless you 486 | entered into that arrangement, or that patent license was granted, prior to 28 487 | March 2007. 488 | 489 | Nothing in this License shall be construed as excluding or limiting any implied 490 | license or other defenses to infringement that may otherwise be available to you 491 | under applicable patent law. 492 | 493 | 12. No Surrender of Others' Freedom. 494 | 495 | If conditions are imposed on you (whether by court order, agreement or 496 | otherwise) that contradict the conditions of this License, they do not excuse 497 | you from the conditions of this License. If you cannot convey a covered work so 498 | as to satisfy simultaneously your obligations under this License and any other 499 | pertinent obligations, then as a consequence you may not convey it at all. For 500 | example, if you agree to terms that obligate you to collect a royalty for 501 | further conveying from those to whom you convey the Program, the only way you 502 | could satisfy both those terms and this License would be to refrain entirely 503 | from conveying the Program. 504 | 505 | 13. Remote Network Interaction; Use with the GNU General Public License. 506 | 507 | Notwithstanding any other provision of this License, if you modify the Program, 508 | your modified version must prominently offer all users interacting with it 509 | remotely through a computer network (if your version supports such interaction) 510 | an opportunity to receive the Corresponding Source of your version by providing 511 | access to the Corresponding Source from a network server at no charge, through 512 | some standard or customary means of facilitating copying of software. This 513 | Corresponding Source shall include the Corresponding Source for any work covered 514 | by version 3 of the GNU General Public License that is incorporated pursuant to 515 | the following paragraph. 516 | 517 | Notwithstanding any other provision of this License, you have permission to link 518 | or combine any covered work with a work licensed under version 3 of the GNU 519 | General Public License into a single combined work, and to convey the resulting 520 | work. The terms of this License will continue to apply to the part which is the 521 | covered work, but the work with which it is combined will remain governed by 522 | version 3 of the GNU General Public License. 523 | 524 | 14. Revised Versions of this License. 525 | 526 | The Free Software Foundation may publish revised and/or new versions of the GNU 527 | Affero General Public License from time to time. Such new versions will be 528 | similar in spirit to the present version, but may differ in detail to address 529 | new problems or concerns. 530 | 531 | Each version is given a distinguishing version number. If the Program specifies 532 | that a certain numbered version of the GNU Affero General Public License "or any 533 | later version" applies to it, you have the option of following the terms and 534 | conditions either of that numbered version or of any later version published by 535 | the Free Software Foundation. If the Program does not specify a version number 536 | of the GNU Affero General Public License, you may choose any version ever 537 | published by the Free Software Foundation. 538 | 539 | If the Program specifies that a proxy can decide which future versions of the 540 | GNU Affero General Public License can be used, that proxy's public statement of 541 | acceptance of a version permanently authorizes you to choose that version for 542 | the Program. 543 | 544 | Later license versions may give you additional or different permissions. 545 | However, no additional obligations are imposed on any author or copyright holder 546 | as a result of your choosing to follow a later version. 547 | 548 | 15. Disclaimer of Warranty. 549 | 550 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 551 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER 552 | PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER 553 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 554 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE 555 | QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 556 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 557 | 558 | 16. Limitation of Liability. 559 | 560 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY 561 | COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS 562 | PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 563 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE 564 | THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED 565 | INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE 566 | PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY 567 | HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 568 | 569 | 17. Interpretation of Sections 15 and 16. 570 | 571 | If the disclaimer of warranty and limitation of liability provided above cannot 572 | be given local legal effect according to their terms, reviewing courts shall 573 | apply local law that most closely approximates an absolute waiver of all civil 574 | liability in connection with the Program, unless a warranty or assumption of 575 | liability accompanies a copy of the Program in return for a fee. 576 | 577 | END OF TERMS AND CONDITIONS 578 | 579 | How to Apply These Terms to Your New Programs 580 | 581 | If you develop a new program, and you want it to be of the greatest possible use 582 | to the public, the best way to achieve this is to make it free software which 583 | everyone can redistribute and change under these terms. 584 | 585 | To do so, attach the following notices to the program. It is safest to attach 586 | them to the start of each source file to most effectively state the exclusion of 587 | warranty; and each file should have at least the "copyright" line and a pointer 588 | to where the full notice is found. 589 | 590 | 591 | Copyright (C) 592 | 593 | This program is free software: you can redistribute it and/or modify 594 | it under the terms of the GNU Affero General Public License as published 595 | by the Free Software Foundation, either version 3 of the License, or 596 | (at your option) any later version. 597 | 598 | This program is distributed in the hope that it will be useful, 599 | but WITHOUT ANY WARRANTY; without even the implied warranty of 600 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 601 | GNU Affero General Public License for more details. 602 | 603 | You should have received a copy of the GNU Affero General Public License 604 | along with this program. If not, see . 605 | 606 | Also add information on how to contact you by electronic and paper mail. 607 | 608 | If your software can interact with users remotely through a computer network, 609 | you should also make sure that it provides a way for users to get its source. 610 | For example, if your program is a web application, its interface could display 611 | a "Source" link that leads users to an archive of the code. There are many ways 612 | you could offer source, and different solutions will be better for different 613 | programs; see section 13 for the specific requirements. 614 | 615 | You should also get your employer (if you work as a programmer) or school, if 616 | any, to sign a "copyright disclaimer" for the program, if necessary. For more 617 | information on this, and how to apply and follow the GNU AGPL, see 618 | . 619 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Enumhancer 2 | 3 | [![Build Status](https://github.com/henzeb/enumhancer/workflows/tests/badge.svg)](https://github.com/henzeb/enumhancer/actions) 4 | [![Test Coverage](https://api.codeclimate.com/v1/badges/5cee34b5dd839b0c2cdd/test_coverage)](https://codeclimate.com/github/henzeb/enumhancer/test_coverage) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/henzeb/enumhancer.svg)](https://packagist.org/packages/henzeb/enumhancer) 6 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/henzeb/enumhancer.svg)](https://packagist.org/packages/henzeb/enumhancer) 7 | [![License](https://img.shields.io/packagist/l/henzeb/enumhancer)](https://packagist.org/packages/henzeb/enumhancer) 8 | 9 | This package is your framework-agnostic Swiss Army knife when it comes to 10 | PHP 8.1's native enums. In this package you will find a lot of tools for 11 | the most common use cases, and more will be added in the future. 12 | 13 | If you have an idea, or you miss something that needs to be added, just let me 14 | know. 15 | 16 | Enumhancer is case-agnostic, which means `Enum` equals `ENUM` equals `enum`. 17 | This is done with the idea that it is useless to have two different enums 18 | having the same name and different casing. 19 | 20 | It is also type-agnostic. This way you can steer clear as much as possible 21 | from the extra work that comes with backed enums. 22 | 23 | Note: While most functionality that also exists in Spatie's PHP Enum is made 24 | backwards compatible to allow for an easy migration to PHP native enums, 25 | currently this is not the case for their laravel package, PHPUnit assertions or 26 | Faker Provider. 27 | 28 | ## Installation 29 | 30 | You can install the package via composer: 31 | 32 | ```bash 33 | composer require henzeb/enumhancer 34 | ``` 35 | 36 | ## Usage 37 | 38 | You can simply add the `Enhancers` trait to your `enum` in order to use almost 39 | all functionality of this package. All features should work with `basic` enums as 40 | well as `backed` enums' unless stated otherwise. 41 | 42 | ```php 43 | use Henzeb\Enumhancer\Concerns\Enhancers; 44 | 45 | enum YourEnum { 46 | use Enhancers; 47 | 48 | // ... 49 | } 50 | ``` 51 | 52 | You can also just use one of the features by using the specific trait for that 53 | feature. 54 | 55 | Note: all traits can be used next to each other, except for `Mappers`, which has 56 | implemented the methods of `Getters`, `Extractor` and `Reporters`. 57 | 58 | ### Features 59 | 60 | - [Attributes](docs/attributes.md) 61 | - [Bitmasks](docs/bitmasks.md) 62 | - [Constructor](docs/constructor.md) 63 | - [Comparison](docs/comparison.md) 64 | - [Configure](docs/configure.md) 65 | - [Defaults](docs/defaults.md) 66 | - [Dropdown](docs/dropdown.md) 67 | - [Extractor](docs/extractor.md) 68 | - [From](docs/from.md) 69 | - [Getters](docs/getters.md) 70 | - [Labels](docs/labels.md) 71 | - [Macros](docs/macros.md) 72 | - [Mappers](docs/mappers.md) 73 | - [Properties](docs/properties.md) 74 | - [Reporters](docs/reporters.md) 75 | - [State](docs/state.md) 76 | - [Subset](docs/subset.md) 77 | - [Value](docs/value.md) 78 | 79 | ### Helper functions 80 | 81 | - [Backing](docs/functions.md#backing) 82 | - [Name](docs/functions.md#name) 83 | - [Value](docs/functions.md#value) 84 | 85 | ### Development 86 | 87 | - [IDE-Helper](docs/ide-helper.md) 88 | - [PHPstan](docs/phpstan.md) 89 | 90 | ### Laravel specific Features 91 | 92 | - [Blade](docs/blade.md) 93 | - [Casting](docs/casting.md) 94 | - [FormRequest](docs/formrequests.md) 95 | - [Implicit (basic) enum binding](docs/binding.md) 96 | - [Validation](docs/laravel.validation.md) 97 | 98 | ### Laravel's auto-discovery 99 | 100 | When you are installing this package into a laravel project, Enumhancer will 101 | automatically set macro's for the `validation rules` and sets the global 102 | `Reporter` for the `getOrReport` methods, so that it will use Laravel's 103 | `Log` facade. 104 | 105 | If you don't want that to happen, you can tell Laravel not to discover the 106 | package. 107 | 108 | ```composer 109 | "extra": { 110 | "laravel": { 111 | "dont-discover": [ 112 | "henzeb/enumhancer" 113 | ] 114 | } 115 | } 116 | ``` 117 | 118 | ### Testing 119 | 120 | ```bash 121 | composer test 122 | ``` 123 | 124 | #### PHPStan integration 125 | 126 | If you are using PHPStan for static analysis, you can enable the extension. 127 | 128 | Add the following to your projects phpstan.neon: 129 | 130 | ```` 131 | includes: 132 | - vendor/henzeb/enumhancer/extension.neon 133 | ```` 134 | 135 | ## Changelog 136 | 137 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed 138 | recently. 139 | 140 | ## Contributing 141 | 142 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 143 | 144 | ### Security 145 | 146 | If you discover any security related issues, please email 147 | henzeberkheij@gmail.com instead of using the issue tracker. 148 | 149 | ## Credits 150 | 151 | - [Henze Berkheij](https://github.com/henzeb) 152 | 153 | ## License 154 | 155 | The GNU AGPLv. Please see [License File](LICENSE.md) for more information. 156 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "henzeb/enumhancer", 3 | "description": "Your framework-agnostic Swiss Army knife for PHP 8.1+ native enums", 4 | "keywords": [ 5 | "henzeb", 6 | "enumhancer", 7 | "enums", 8 | "enum", 9 | "dropdown", 10 | "select", 11 | "list", 12 | "enumerators", 13 | "enumerator", 14 | "mappers", 15 | "labels", 16 | "reporting", 17 | "report", 18 | "logging", 19 | "stringable", 20 | "string", 21 | "comparison", 22 | "blade", 23 | "native", 24 | "backed", 25 | "basic", 26 | "unit", 27 | "unitEnum", 28 | "backedEnum", 29 | "from", 30 | "tryFrom", 31 | "extract", 32 | "extractor", 33 | "properties", 34 | "subset", 35 | "value", 36 | "casting", 37 | "cast", 38 | "eloquent", 39 | "laravel", 40 | "spatie", 41 | "default", 42 | "php 8.1", 43 | "8.1", 44 | "php 8.2", 45 | "8.2", 46 | "state", 47 | "machine", 48 | "transition", 49 | "validation", 50 | "rules", 51 | "bitmask", 52 | "bitmasks", 53 | "macros", 54 | "macroable", 55 | "binding", 56 | "ide-helper", 57 | "implicit" 58 | ], 59 | "homepage": "https://github.com/henzeb/enumhancer", 60 | "license": "AGPL-3.0-only", 61 | "type": "library", 62 | "authors": [ 63 | { 64 | "name": "Henze Berkheij", 65 | "email": "henzeberkheij@gmail.com", 66 | "role": "Developer" 67 | } 68 | ], 69 | "require": { 70 | "php": "^8.1" 71 | }, 72 | "require-dev": { 73 | "composer/composer": "2.8.9", 74 | "henzeb/enumhancer-ide-helper": "main-dev", 75 | "mockery/mockery": "^1.5", 76 | "orchestra/testbench": "^8|^9|^10", 77 | "pestphp/pest": "^2.0|^3.0", 78 | "phpstan/phpstan": "^2.0" 79 | }, 80 | "autoload": { 81 | "files": [ 82 | "src/Functions/Functions.php" 83 | ], 84 | "psr-4": { 85 | "Henzeb\\Enumhancer\\": "src/" 86 | } 87 | }, 88 | "autoload-dev": { 89 | "psr-4": { 90 | "Henzeb\\Enumhancer\\Tests\\": "tests/" 91 | } 92 | }, 93 | "scripts": { 94 | "test": "vendor/bin/pest", 95 | "test-coverage-txt": "XDEBUG_MODE=coverage vendor/bin/pest --coverage --coverage-text", 96 | "test-coverage": "XDEBUG_MODE=coverage vendor/bin/pest --coverage --coverage-html coverage", 97 | "test-dox": "vendor/bin/pest --testdox" 98 | }, 99 | "config": { 100 | "sort-packages": true, 101 | "allow-plugins": { 102 | "infection/extension-installer": true, 103 | "pestphp/pest-plugin": true 104 | } 105 | }, 106 | "extra": { 107 | "laravel": { 108 | "providers": [ 109 | "Henzeb\\Enumhancer\\Laravel\\Providers\\EnumhancerServiceProvider" 110 | ] 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | rules: 2 | - Henzeb\Enumhancer\PHPStan\Constants\Rules\DefaultConstantRule 3 | - Henzeb\Enumhancer\PHPStan\Constants\Rules\MapperConstantRule 4 | - Henzeb\Enumhancer\PHPStan\Constants\Rules\StrictConstantRule 5 | 6 | services: 7 | - 8 | class: Henzeb\Enumhancer\PHPStan\Constants\DefaultConstantAlwaysUsed 9 | tags: 10 | - phpstan.constants.alwaysUsedClassConstantsExtension 11 | 12 | - 13 | class: Henzeb\Enumhancer\PHPStan\Constants\BitmaskConstantAlwaysUsed 14 | tags: 15 | - phpstan.constants.alwaysUsedClassConstantsExtension 16 | 17 | - 18 | class: Henzeb\Enumhancer\PHPStan\Constants\StrictConstantAlwaysUsed 19 | tags: 20 | - phpstan.constants.alwaysUsedClassConstantsExtension 21 | 22 | - 23 | class: Henzeb\Enumhancer\PHPStan\Constants\MapperConstantAlwaysUsed 24 | tags: 25 | - phpstan.constants.alwaysUsedClassConstantsExtension 26 | 27 | - 28 | class: Henzeb\Enumhancer\PHPStan\Methods\EnumMacrosMethodsClassReflection 29 | tags: 30 | - phpstan.broker.methodsClassReflectionExtension 31 | 32 | - 33 | class: Henzeb\Enumhancer\PHPStan\Methods\EnumComparisonMethodsClassReflection 34 | tags: 35 | - phpstan.broker.methodsClassReflectionExtension 36 | 37 | - 38 | class: Henzeb\Enumhancer\PHPStan\Methods\EnumConstructorMethodsClassReflection 39 | tags: 40 | - phpstan.broker.methodsClassReflectionExtension 41 | 42 | - 43 | class: Henzeb\Enumhancer\PHPStan\Methods\EnumStateMethodsClassReflection 44 | tags: 45 | - phpstan.broker.methodsClassReflectionExtension 46 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | - vendor/nunomaduro/larastan/extension.neon 4 | - vendor/phpstan/phpstan/conf/bleedingEdge.neon 5 | parameters: 6 | scanDirectories: 7 | - src 8 | paths: 9 | - src 10 | level: 6 11 | -------------------------------------------------------------------------------- /src/Composer/IdeHelper.php: -------------------------------------------------------------------------------- 1 | isDevMode()) { 18 | return; 19 | } 20 | 21 | $composer = $event->getComposer(); 22 | $config = $composer->getConfig(); 23 | $package = $composer->getPackage(); 24 | $vendorDir = $config->get('vendor-dir'); 25 | 26 | self::requireAutoloader($vendorDir); 27 | 28 | $alreadyBootstrapped = self::requireUserDefinedBootstrap($package); 29 | 30 | if (!$alreadyBootstrapped) { 31 | self::requireLaravel($package); 32 | } 33 | 34 | if (self::hasIdeHelperInstalled($package)) { 35 | EnumIdeHelper::postAutoloadDump($event); 36 | } 37 | } 38 | 39 | private static function hasIdeHelperInstalled(RootPackageInterface $package): bool 40 | { 41 | $filtered = array_filter( 42 | $package->getDevRequires(), 43 | fn($name) => $name === 'henzeb/enumhancer-ide-helper', 44 | ARRAY_FILTER_USE_KEY 45 | ); 46 | 47 | return count($filtered) > 0; 48 | } 49 | 50 | private static function requireAutoloader(string $vendorDir): void 51 | { 52 | $file = $vendorDir . '/autoload.php'; 53 | 54 | if (file_exists($vendorDir . '/autoload_runtime.php')) { 55 | $file = $vendorDir . '/autoload_runtime.php'; 56 | } 57 | 58 | require_once $file; 59 | } 60 | 61 | private static function requireUserDefinedBootstrap(RootPackageInterface $package): bool 62 | { 63 | $file = $package->getExtra()['enumhancer']['ide-helper'] ?? null; 64 | 65 | if ($file) { 66 | if (!file_exists($file)) { 67 | throw new RuntimeException( 68 | sprintf( 69 | 'require_once(%s): Failed to open stream: No such file or directory', 70 | $file 71 | ) 72 | ); 73 | } 74 | require_once $file; 75 | return true; 76 | } 77 | return false; 78 | } 79 | 80 | private static function requireLaravel(RootPackageInterface $package): void 81 | { 82 | $filtered = array_filter( 83 | $package->getRequires(), 84 | fn(string $name) => $name === 'laravel/framework', 85 | ARRAY_FILTER_USE_KEY 86 | ); 87 | $realPath = realpath('./bootstrap/app.php'); 88 | $hasLaravel = count($filtered) > 0 89 | && is_string($realPath) 90 | && file_exists($realPath); 91 | 92 | if ($hasLaravel) { 93 | $app = require_once realpath('./bootstrap/app.php'); 94 | 95 | /** 96 | * already bootstrapped 97 | */ 98 | if (is_bool($app)) { 99 | return; 100 | } 101 | 102 | /** 103 | * @var Kernel $kernel 104 | */ 105 | $kernel = $app->make(Kernel::class); 106 | 107 | $kernel->bootstrap(); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Concerns/Attributes.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public static function bits(): array 19 | { 20 | return EnumBitmasks::getCaseBits(self::class); 21 | } 22 | 23 | public static function mask(self|string|int ...$enums): Bitmask 24 | { 25 | return EnumBitmasks::getMask(self::class, ...$enums); 26 | } 27 | 28 | public static function fromMask(int $mask): Bitmask 29 | { 30 | return EnumBitmasks::fromMask(self::class, $mask); 31 | } 32 | 33 | public static function tryMask( 34 | ?int $mask, 35 | BitMask|self|string|int|null ...$enums 36 | ): Bitmask { 37 | return EnumBitmasks::tryMask( 38 | self::class, 39 | $mask, 40 | ...$enums 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Concerns/Comparison.php: -------------------------------------------------------------------------------- 1 | equals($equals); 20 | } 21 | 22 | public function isNot(UnitEnum|string|int|null $equals): bool 23 | { 24 | return !$this->is($equals); 25 | } 26 | 27 | public function isIn(UnitEnum|string|int|null ...$equals): bool 28 | { 29 | return $this->equals(...$equals); 30 | } 31 | 32 | public function isNotIn(UnitEnum|string|int|null ...$equals): bool 33 | { 34 | return !$this->equals(...$equals); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Concerns/Configure.php: -------------------------------------------------------------------------------- 1 | isDefault(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Concerns/Dropdown.php: -------------------------------------------------------------------------------- 1 | dropdown($keepEnumCase); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Concerns/Enhancers.php: -------------------------------------------------------------------------------- 1 | isTransitionAllowed($state, $hook)) { 28 | $hook?->execute($this, $state); 29 | self::transitionHook()?->execute($this, $state); 30 | 31 | return $state; 32 | } 33 | 34 | throw new IllegalEnumTransitionException($this, $state); 35 | } 36 | 37 | /** 38 | * @param string|int|static $state 39 | * @param TransitionHook|null $hook 40 | * @return static 41 | * @throws IllegalEnumTransitionException 42 | */ 43 | public function to(self|string|int $state, TransitionHook|null $hook = null): static 44 | { 45 | return $this->transitionTo($state, $hook); 46 | } 47 | 48 | public function tryTo(self|string|int $state, TransitionHook|null $hook = null): static 49 | { 50 | if ($this->isTransitionAllowed($state, $hook)) { 51 | return $this->transitionTo($state, $hook); 52 | } 53 | return $this; 54 | } 55 | 56 | /** 57 | * @param self|string|int $state 58 | * @param TransitionHook|null $hook 59 | * @return bool 60 | */ 61 | public function isTransitionAllowed(self|string|int $state, TransitionHook|null $hook = null): bool 62 | { 63 | /** 64 | * @var $this UnitEnum 65 | */ 66 | $state = EnumGetters::tryCast(self::class, $state); 67 | 68 | return $state !== null && in_array($state, $this->allowedTransitions($hook)); 69 | } 70 | 71 | /** 72 | * @param TransitionHook|null $hook 73 | * @return static[] 74 | */ 75 | public function allowedTransitions(TransitionHook|null $hook = null): array 76 | { 77 | return EnumState::allowedTransitions( 78 | $this, 79 | ...array_filter([$hook, self::transitionHook()]) 80 | ); 81 | } 82 | 83 | /** 84 | * @return static[] 85 | */ 86 | public static function transitions(): array 87 | { 88 | return EnumState::transitions(self::class, self::customTransitions()); 89 | } 90 | 91 | /** 92 | * @return static[] 93 | */ 94 | protected static function customTransitions(): array 95 | { 96 | return EnumProperties::get(self::class, EnumProperties::reservedWord('state')) ?? []; 97 | } 98 | 99 | protected static function transitionHook(): ?TransitionHook 100 | { 101 | return EnumProperties::get( 102 | self::class, 103 | EnumProperties::reservedWord('hooks') 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Concerns/Subset.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public static function without(self ...$enums): EnumSubsetMethods 15 | { 16 | return new EnumSubsetMethods( 17 | self::class, 18 | ...array_filter( 19 | self::cases(), 20 | function (UnitEnum $case) use ($enums) { 21 | return !in_array($case, $enums); 22 | } 23 | ) 24 | ); 25 | } 26 | 27 | /** 28 | * @param static[] $enums 29 | * @return EnumSubsetMethods 30 | */ 31 | public static function of(self ...$enums): EnumSubsetMethods 32 | { 33 | return new EnumSubsetMethods( 34 | self::class, 35 | ...($enums ?: self::cases()) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Concerns/Value.php: -------------------------------------------------------------------------------- 1 | > 24 | */ 25 | abstract protected function mappable(): array; 26 | 27 | public function makeFlipped(string|null $prefix = null): self 28 | { 29 | $this->flip = true; 30 | $this->flipped = null; 31 | $this->flipPrefix = $prefix; 32 | 33 | return $this; 34 | } 35 | 36 | private function flipMethod(string|null $prefix = null): self 37 | { 38 | return (clone $this)->makeFlipped($prefix); 39 | } 40 | 41 | private function parseValue(mixed $value): string|int|null 42 | { 43 | if (null === $value) { 44 | $value = null; 45 | } 46 | 47 | if (empty($value)) { 48 | $value = null; 49 | } 50 | 51 | if ($value instanceof UnitEnum) { 52 | $value = $value->name; 53 | } 54 | 55 | if (!is_string($value) && !is_int($value)) { 56 | $value = null; 57 | } 58 | 59 | return $value; 60 | } 61 | 62 | private function getMapWithPrefix(string|null $prefix = null): array 63 | { 64 | /** 65 | * @var array $mappable 66 | */ 67 | $mappable = $this->mappable(); 68 | return array_change_key_case($mappable[$prefix] ?? []); 69 | } 70 | 71 | private function getMap(string|null $prefix = null): array 72 | { 73 | if ($this->flip) { 74 | return $this->flipped ?? $this->flipped = $this->flipMappable($prefix); 75 | } 76 | 77 | return array_change_key_case($this->mappable()); 78 | } 79 | 80 | private function mapMethod(string|UnitEnum $key, string|null $prefix = null): string|int|null 81 | { 82 | $key = $this->parseValue($key); 83 | 84 | if (is_string($key)) { 85 | $key = strtolower($key); 86 | } 87 | 88 | return $this->parseValue( 89 | ($this->flip ? null : $this->getMapWithPrefix($prefix)[$key] ?? null) 90 | ?? 91 | $this->getMap($prefix)[$key] 92 | ?? null 93 | ); 94 | } 95 | 96 | private function definedMethod(string|UnitEnum $key, string|null $prefix = null): bool 97 | { 98 | return (bool)$this->map($key, $prefix); 99 | } 100 | 101 | private function keysMethod(string|null $prefix = null): array 102 | { 103 | if (!$prefix || $this->flip) { 104 | $mappable = $this->getMap($prefix); 105 | } 106 | 107 | if (!isset($mappable)) { 108 | $mappable = [...$this->getMap(), ...$this->getMapWithPrefix($prefix)]; 109 | } 110 | 111 | return array_unique( 112 | array_merge( 113 | array_keys( 114 | array_filter( 115 | $mappable, 116 | function ($value) { 117 | return !is_array($value); 118 | } 119 | ), 120 | ), 121 | is_array($mappable[$prefix] ?? null) ? array_keys($mappable[$prefix]) : [] 122 | ) 123 | ); 124 | } 125 | 126 | private function flipMappable(string|null $prefix = null): array 127 | { 128 | return array_change_key_case( 129 | array_flip( 130 | array_filter( 131 | array_map( 132 | fn($value) => is_array($value) ? null : $this->parseValue($value), 133 | $this->getMapWithPrefix($prefix ?? $this->flipPrefix) ?: $this->mappable() 134 | ) 135 | ) 136 | ) 137 | ); 138 | } 139 | 140 | public function __call(string $name, array $arguments): mixed 141 | { 142 | return match ($name) { 143 | 'map' => $this->mapMethod(...$arguments), 144 | 'defined' => $this->definedMethod(...$arguments), 145 | 'keys' => $this->keysMethod(...$arguments), 146 | 'flip' => $this->flipMethod(...$arguments), 147 | default => $this->triggerError($name) 148 | }; 149 | } 150 | 151 | public static function __callStatic(string $name, array $arguments): mixed 152 | { 153 | return self::newInstance()->$name(...$arguments); 154 | } 155 | 156 | private function triggerError(string $name): bool 157 | { 158 | return throw new ErrorException( 159 | sprintf( 160 | 'Uncaught Error: Call to undefined method %s::%s()', 161 | static::class, 162 | $name 163 | ), 164 | E_USER_ERROR 165 | ); 166 | } 167 | 168 | public static function newInstance(mixed ...$parameters): static 169 | { 170 | return new static(...$parameters); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Contracts/Reporter.php: -------------------------------------------------------------------------------- 1 | getMethodName($from, $transitionTo); 13 | 14 | if (method_exists($this, $method)) { 15 | $this->$method(); 16 | } 17 | } 18 | 19 | /** 20 | * @throws SyntaxException 21 | */ 22 | final public function isAllowed(UnitEnum $from, UnitEnum $transitionTo): bool 23 | { 24 | $method = $this->getMethodName($from, $transitionTo, 'allows'); 25 | 26 | if (method_exists($this, $method)) { 27 | $value = $this->$method(); 28 | if (is_bool($value)) { 29 | return $value; 30 | } 31 | 32 | if (!is_null($value)) { 33 | throw new SyntaxException('true', $value); 34 | } 35 | } 36 | 37 | return true; 38 | } 39 | 40 | private function getMethodName(UnitEnum $from, UnitEnum $transitionTo, string $prefix = ''): string 41 | { 42 | return $prefix . $from->name . $transitionTo->name; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Enums/LogLevel.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | enum LogLevel 13 | { 14 | use Enhancers, Configure, Macros; 15 | 16 | case Debug; 17 | case Info; 18 | case Notice; 19 | case Warning; 20 | case Error; 21 | case Critical; 22 | case Alert; 23 | case Emergency; 24 | } 25 | -------------------------------------------------------------------------------- /src/Exceptions/EnumException.php: -------------------------------------------------------------------------------- 1 | name, $transitionTo->name) 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidBitmaskEnum.php: -------------------------------------------------------------------------------- 1 | name; 54 | } 55 | 56 | function name(?UnitEnum $enum): ?string 57 | { 58 | return n($enum); 59 | } 60 | 61 | /** 62 | * Returns a value equal to it's name when given enum is not backed by default 63 | * 64 | * @param UnitEnum|null $enum 65 | * @param bool $keepValueCase returns a lower cased enum name when it's a UnitEnum 66 | * @return string|int|null 67 | */ 68 | function v(?UnitEnum $enum, bool $keepValueCase = true): string|int|null 69 | { 70 | if (!$enum) { 71 | return null; 72 | } 73 | return EnumValue::value($enum, $keepValueCase); 74 | } 75 | 76 | 77 | function value(?UnitEnum $enum, bool $keepValueCase = true): string|int|null 78 | { 79 | return v($enum, $keepValueCase); 80 | } 81 | 82 | /** 83 | * Returns a lowercase value equal to it's name when given enum is not backed. 84 | * 85 | * @param UnitEnum|null $enum 86 | * @return string|int|null 87 | */ 88 | function vl(?UnitEnum $enum): string|int|null 89 | { 90 | return v($enum, false); 91 | } 92 | 93 | function valueLowercase(?UnitEnum $enum): string|int|null 94 | { 95 | return vl($enum); 96 | } 97 | -------------------------------------------------------------------------------- /src/Helpers/Bitmasks/Bitmask.php: -------------------------------------------------------------------------------- 1 | bitmask); 18 | } 19 | 20 | public function has(UnitEnum|string|int $enum): bool 21 | { 22 | return $this->all($enum); 23 | } 24 | 25 | public function all(self|UnitEnum|string|int ...$bits): bool 26 | { 27 | $mask = EnumBitmasks::getBits($this->enumFQCN, ...$bits); 28 | 29 | if ($mask === 0) { 30 | return true; 31 | } 32 | 33 | return ($this->value() & $mask) === $mask; 34 | } 35 | 36 | public function any(self|UnitEnum|string|int ...$bits): bool 37 | { 38 | $mask = EnumBitmasks::getBits($this->enumFQCN, ...$bits); 39 | 40 | if ($mask === 0) { 41 | return true; 42 | } 43 | 44 | foreach ($bits as $bit) { 45 | if ($this->has($bit)) { 46 | return true; 47 | } 48 | } 49 | 50 | return false; 51 | } 52 | 53 | public function xor(self|UnitEnum|string|int ...$bits): bool 54 | { 55 | if (count($bits) === 0) { 56 | return false; 57 | } 58 | 59 | $result = false; 60 | 61 | foreach ($bits as $bit) { 62 | $hasBit = $this->has($bit); 63 | 64 | if ($hasBit && $result) { 65 | return false; 66 | } 67 | 68 | if ($hasBit) { 69 | $result = true; 70 | } 71 | } 72 | 73 | return $result; 74 | } 75 | 76 | public function none(self|UnitEnum|string|int ...$bits): bool 77 | { 78 | $mask = EnumBitmasks::getBits($this->enumFQCN, ...$bits); 79 | 80 | if ($mask === 0) { 81 | return true; 82 | } 83 | 84 | return !$this->any(...$bits); 85 | } 86 | 87 | public function value(): int 88 | { 89 | return $this->bitmask; 90 | } 91 | 92 | public function cases(): array 93 | { 94 | $matchingCases = []; 95 | 96 | foreach ($this->enumFQCN::cases() as $case) { 97 | $value = EnumBitmasks::getBit($case); 98 | if ($this->bitmask === $value) { 99 | return [$case]; 100 | } 101 | if ($this->bitmask & $value) { 102 | $matchingCases[] = $case; 103 | } 104 | } 105 | 106 | return $matchingCases; 107 | } 108 | 109 | public function __toString(): string 110 | { 111 | return (string)$this->value(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Helpers/Bitmasks/Concerns/BitmaskModifiers.php: -------------------------------------------------------------------------------- 1 | bitmask |= EnumBitmasks::getBits($this->enumFQCN, ...$enums); 13 | 14 | return $this; 15 | } 16 | 17 | public function unset(self|UnitEnum|string|int ...$enums): self 18 | { 19 | $this->bitmask &= ~EnumBitmasks::getBits($this->enumFQCN, ...$enums); 20 | 21 | return $this; 22 | } 23 | 24 | public function toggle(self|UnitEnum|string|int ...$enums): self 25 | { 26 | foreach ($enums as $enum) { 27 | $this->has($enum) ? $this->unset($enum) : $this->set($enum); 28 | } 29 | 30 | return $this; 31 | } 32 | 33 | public function clear(): self 34 | { 35 | $this->bitmask = 0; 36 | 37 | return $this; 38 | } 39 | 40 | public function copy(): self 41 | { 42 | return new self( 43 | $this->forEnum(), 44 | $this->value() 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Helpers/Bitmasks/Concerns/BitmaskValidators.php: -------------------------------------------------------------------------------- 1 | enumFQCN === $enumclass; 15 | } 16 | 17 | public function forOrFail(string $enumClass): bool 18 | { 19 | if ($this->for($enumClass)) { 20 | return true; 21 | } 22 | 23 | EnumBitmasks::throwMismatch( 24 | $this->forEnum(), 25 | $enumClass 26 | ); 27 | } 28 | 29 | public function forEnum(): string 30 | { 31 | return $this->enumFQCN; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Helpers/Bitmasks/EnumBitmasks.php: -------------------------------------------------------------------------------- 1 | > 1); 31 | } 32 | 33 | public static function isBit(mixed $bit): bool 34 | { 35 | return self::isInt($bit) && (self::countSetBits($bit) === 1 || $bit === 0); 36 | } 37 | 38 | public static function validateBitmaskCases(string $enum): void 39 | { 40 | EnumCheck::check($enum); 41 | 42 | if (in_array($enum, self::$isValid) 43 | || !is_a($enum, BackedEnum::class, true) 44 | || self::ignoreIntValues($enum) 45 | ) { 46 | self::$isValid[] = $enum; 47 | self::$isValid = array_unique(self::$isValid); 48 | return; 49 | } 50 | 51 | self::validateBitCases($enum); 52 | 53 | self::$isValid[] = $enum; 54 | } 55 | 56 | public static function ignoreIntValues(string $enum): bool 57 | { 58 | /** 59 | * @var UnitEnum $enum 60 | */ 61 | 62 | EnumCheck::check($enum); 63 | 64 | foreach ((new ReflectionClass($enum))->getConstants() as $constant => $value) { 65 | if (strtolower($constant) === 'bit_values' and is_bool($value)) { 66 | return !$value; 67 | } 68 | } 69 | return true; 70 | } 71 | 72 | public static function isModifier(BackedEnum|string $enum): bool 73 | { 74 | /** 75 | * @var UnitEnum $enum 76 | */ 77 | 78 | EnumCheck::check($enum); 79 | 80 | if (self::ignoreIntValues($enum)) { 81 | return false; 82 | } 83 | 84 | foreach ((new ReflectionClass($enum))->getConstants() as $constant => $value) { 85 | if (strtolower($constant) === 'bit_modifier' and is_bool($value)) { 86 | return $value; 87 | } 88 | } 89 | return false; 90 | } 91 | 92 | private static function validateBitCases(BackedEnum|string $enum): void 93 | { 94 | if (self::isModifier($enum)) { 95 | return; 96 | } 97 | 98 | foreach ($enum::cases() as $case) { 99 | if (self::validateBitCase($case)) { 100 | self::triggerInvalidBitCase($case::class, $case); 101 | } 102 | } 103 | } 104 | 105 | private static function validateBitCase(BackedEnum $case): bool 106 | { 107 | return self::isInt($case->value) && !self::isBit($case->value); 108 | } 109 | 110 | public static function getBit(UnitEnum $enum): int 111 | { 112 | self::validateBitmaskCases($enum::class); 113 | 114 | $value = EnumValue::value($enum); 115 | 116 | if (self::ignoreIntValues($enum::class) 117 | || !is_int($value) 118 | ) { 119 | return pow( 120 | 2, 121 | (int)array_search($enum, $enum::cases()) 122 | ); 123 | } 124 | 125 | return $value; 126 | } 127 | 128 | public static function getMask(string $class, UnitEnum|string|int ...$enums): Bitmask 129 | { 130 | return new Bitmask( 131 | $class, 132 | self::getBits($class, ...$enums) 133 | ); 134 | } 135 | 136 | public static function getBits(string|UnitEnum $class, Bitmask|UnitEnum|string|int ...$values): int 137 | { 138 | $bits = 0; 139 | 140 | foreach ($values as $value) { 141 | $bits |= self::castToBits($value, $class); 142 | } 143 | 144 | return $bits; 145 | } 146 | 147 | private static function castToBits(Bitmask|UnitEnum|string|int $value, string|UnitEnum $class): int 148 | { 149 | $class = is_object($class) ? $class::class : $class; 150 | 151 | if ($value instanceof Bitmask) { 152 | self::forOrFail($class, $value); 153 | return $value->value(); 154 | } 155 | 156 | $enum = EnumGetters::tryGet($class, $value); 157 | 158 | if ($enum) { 159 | return self::getBit($enum); 160 | } 161 | 162 | if (self::isInt($value) && self::isValidBitmask($class, $value)) { 163 | /** 164 | * @var int $value 165 | */ 166 | return $value; 167 | } 168 | 169 | self::throwMismatch($class, gettype($value)); 170 | } 171 | 172 | /** 173 | * @param string $class 174 | * @return array 175 | */ 176 | public static function getCaseBits(string $class): array 177 | { 178 | /** 179 | * @var UnitEnum|string $class 180 | */ 181 | $bits = []; 182 | 183 | foreach ($class::cases() as $bit) { 184 | $bits[self::getBit($bit)] = EnumLabels::getLabelOrName($bit); 185 | } 186 | 187 | return $bits; 188 | } 189 | 190 | public static function fromMask(string $enum, int $mask): Bitmask 191 | { 192 | /** 193 | * @var $enum UnitEnum|string 194 | */ 195 | 196 | return new Bitmask( 197 | $enum, 198 | $mask 199 | ); 200 | } 201 | 202 | public static function tryMask(string $enum, ?int $mask, Bitmask|UnitEnum|string|int|null ...$enums): Bitmask 203 | { 204 | /** 205 | * @var $enum UnitEnum|string 206 | */ 207 | 208 | if (!is_null($mask) && self::isValidBitmask($enum, $mask)) { 209 | return new Bitmask($enum, $mask); 210 | } 211 | 212 | return new Bitmask( 213 | $enum, 214 | self::getBits( 215 | $enum, 216 | ...array_filter( 217 | $enums ?: [EnumDefaults::default($enum)] 218 | ) 219 | ), 220 | ); 221 | } 222 | 223 | public static function validateBitmaskOrThrowException(UnitEnum|string $enum, int $bitmask): void 224 | { 225 | if (!self::isValidBitmask($enum, $bitmask)) { 226 | throw new InvalidBitmaskEnum( 227 | is_object($enum) ? $enum::class : $enum, 228 | $bitmask 229 | ); 230 | } 231 | } 232 | 233 | public static function isValidBitmask(UnitEnum|string $enum, mixed $bitmask): bool 234 | { 235 | if (!self::isInt($bitmask)) { 236 | return false; 237 | } 238 | 239 | $maxbits = self::getBits($enum, ...$enum::cases()); 240 | 241 | if ($maxbits < $bitmask) { 242 | return false; 243 | } 244 | 245 | return (int)$bitmask === ($maxbits & $bitmask); 246 | } 247 | 248 | private static function forOrFail(string $class, Bitmask $enum): void 249 | { 250 | if (!$enum->for($class)) { 251 | self::throwMismatch( 252 | $class, 253 | $enum->forEnum() 254 | ); 255 | } 256 | } 257 | 258 | public static function throwMismatch(string $expected, string $given): never 259 | { 260 | EnumCheck::check($expected); 261 | EnumCheck::check($given); 262 | 263 | throw new InvalidBitmaskEnum( 264 | $expected, 265 | $given 266 | ); 267 | } 268 | 269 | protected static function triggerInvalidBitCase(UnitEnum|string $enum, UnitEnum $case): never 270 | { 271 | $enum = is_string($enum) ? $enum : $enum::class; 272 | throw new TypeError( 273 | sprintf('%s::%s is not a valid bit value', $enum, $case->name), 274 | E_USER_ERROR 275 | ); 276 | } 277 | 278 | protected static function isInt(mixed $value): bool 279 | { 280 | return is_scalar($value) && filter_var($value, FILTER_VALIDATE_INT) !== false; 281 | } 282 | 283 | public static function triggerNotImplementingBitmasks(string $enum): never 284 | { 285 | throw new ErrorException( 286 | sprintf('`%s` is not implementing `Bitmasks`', $enum), 287 | E_USER_ERROR 288 | ); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/Helpers/EnumAttributes.php: -------------------------------------------------------------------------------- 1 | name)) 17 | ->getAttributes($attributeClass, ReflectionAttribute::IS_INSTANCEOF); 18 | 19 | if (count($enumAttributes) > 0) { 20 | return $enumAttributes[0]->newInstance(); 21 | } 22 | 23 | return null; 24 | } 25 | 26 | public static function fromCaseArray(string $enumClass, UnitEnum $case, string|null $attributeClass = null): array 27 | { 28 | EnumCheck::check($case, $enumClass); 29 | 30 | $enumAttributes = (new ReflectionClassConstant($enumClass, $case->name)) 31 | ->getAttributes($attributeClass, ReflectionAttribute::IS_INSTANCEOF); 32 | 33 | return array_map( 34 | fn($enumAttribute) => $enumAttribute->newInstance(), 35 | $enumAttributes 36 | ); 37 | } 38 | 39 | public static function fromEnum(string $enumClass, string $attributeClass): mixed 40 | { 41 | EnumCheck::check($enumClass); 42 | 43 | $enumAttributes = (new ReflectionClass($enumClass)) 44 | ->getAttributes($attributeClass, ReflectionAttribute::IS_INSTANCEOF); 45 | 46 | if (count($enumAttributes) > 0) { 47 | return $enumAttributes[0]->newInstance(); 48 | } 49 | 50 | return null; 51 | } 52 | 53 | public static function fromEnumArray(string $enumClass, string|null $attributeClass = null): array 54 | { 55 | EnumCheck::check($enumClass); 56 | 57 | $enumAttributes = (new ReflectionClass($enumClass)) 58 | ->getAttributes($attributeClass, ReflectionAttribute::IS_INSTANCEOF); 59 | 60 | return array_map( 61 | fn($enumAttribute) => $enumAttribute->newInstance(), 62 | $enumAttributes 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Helpers/EnumBlade.php: -------------------------------------------------------------------------------- 1 | equals(...$with); 17 | } 18 | 19 | public static function isValidCall(string $class, string $name, array $arguments): bool 20 | { 21 | EnumCheck::check($class); 22 | 23 | return EnumImplements::comparison($class) 24 | && !count($arguments) && str_starts_with(strtolower($name), 'is') 25 | && self::getValueFromString($class, $name); 26 | } 27 | 28 | private static function getValueFromString(string $class, string $name): ?UnitEnum 29 | { 30 | return EnumGetters::tryGet( 31 | $class, 32 | substr($name, str_starts_with(strtolower($name), 'isnot') ? 5 : 2), 33 | true, 34 | false 35 | ); 36 | } 37 | 38 | public static function call(UnitEnum $enum, string $name): bool 39 | { 40 | $value = self::getValueFromString($enum::class, $name); 41 | 42 | return str_starts_with($name, 'isNot') !== self::equals($enum, $value); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Helpers/EnumDefaults.php: -------------------------------------------------------------------------------- 1 | getFileName() ?: ''; 35 | 36 | return !str_contains($fileName, 'Henzeb/Enumhancer') 37 | && !str_ends_with($fileName, 'Defaults.php'); 38 | } 39 | 40 | private static function getConfiguredOrCustomDefault(string $class): ?UnitEnum 41 | { 42 | $configured = EnumProperties::get($class, EnumProperties::reservedWord('defaults')); 43 | 44 | $hasCustomMethod = self::hasCustomDefaultMethod($class); 45 | 46 | if ($configured && !$hasCustomMethod) { 47 | return $configured; 48 | } 49 | 50 | if ($hasCustomMethod) { 51 | /** 52 | * @var $class UnitEnum|Defaults 53 | */ 54 | return $class::default(); 55 | } 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Helpers/EnumExtractor.php: -------------------------------------------------------------------------------- 1 | $map->keys($class), $mappers) 29 | ) 30 | ); 31 | 32 | preg_match_all(sprintf('/\b%s\b/i', $match), $text, $matches); 33 | 34 | $matches = array_map(fn($value) => EnumMapper::map($class, $value, ...$mappers), $matches[0] ?? []); 35 | 36 | return EnumGetters::getArray($class, $matches); 37 | } 38 | 39 | /** 40 | * @param UnitEnum|string $class 41 | * @return array|string[] 42 | */ 43 | protected static function getMatchArray(UnitEnum|string $class): array 44 | { 45 | /** 46 | * @var UnitEnum|string $class 47 | */ 48 | $match = array_map( 49 | function ($enum) { 50 | 51 | if (property_exists($enum, 'value')) { 52 | return $enum->value; 53 | } 54 | 55 | return $enum->name; 56 | }, 57 | $class::cases() 58 | ); 59 | return $match; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Helpers/EnumGetters.php: -------------------------------------------------------------------------------- 1 | name ?? $value; 29 | 30 | if (($useDefault) 31 | && is_string($value) 32 | && strtolower($value) === 'default' 33 | ) { 34 | return EnumDefaults::default($class) ?? self::throwError($value, $class); 35 | } 36 | 37 | if ($useMapper && EnumImplements::mappers($class)) { 38 | /** 39 | * @var $class Mappers|UnitEnum; 40 | */ 41 | return $class::get($value); 42 | } 43 | 44 | $value = is_object($value) ? $value->name : $value; 45 | 46 | $match = !is_null($value) ? self::match($class, $value) : null; 47 | 48 | if ($match) { 49 | return $match; 50 | } 51 | 52 | self::throwError($value, $class); 53 | } 54 | 55 | public static function tryGet( 56 | string $class, 57 | int|string|UnitEnum|null $value, 58 | bool $useMapper = false, 59 | bool $useDefault = true 60 | ): mixed { 61 | EnumCheck::check($class); 62 | 63 | try { 64 | return self::get($class, $value, $useMapper, $useDefault); 65 | } catch (ValueError) { 66 | return $useDefault ? EnumDefaults::default($class) : null; 67 | } 68 | } 69 | 70 | public static function getArray( 71 | string $class, 72 | iterable $values, 73 | bool $useMapper = false 74 | ): array { 75 | EnumCheck::check($class); 76 | $return = []; 77 | 78 | foreach ($values as $value) { 79 | $return[] = self::get($class, $value, $useMapper); 80 | } 81 | 82 | return $return; 83 | } 84 | 85 | public static function tryArray( 86 | string $class, 87 | iterable $values, 88 | bool $useMapper = false, 89 | bool $useDefault = true 90 | ): array { 91 | EnumCheck::check($class); 92 | 93 | $return = []; 94 | 95 | foreach ($values as $value) { 96 | $return[] = self::tryGet($class, $value, $useMapper, $useDefault); 97 | } 98 | 99 | return array_filter($return); 100 | } 101 | 102 | public static function cast(string|UnitEnum $class, UnitEnum|string|int $enum): mixed 103 | { 104 | EnumCheck::check($class); 105 | 106 | if ($enum instanceof $class) { 107 | return $enum; 108 | } 109 | 110 | return self::get(is_object($class) ? $class::class : $class, $enum, true); 111 | } 112 | 113 | public static function tryCast(string|UnitEnum $class, UnitEnum|int|string $key): mixed 114 | { 115 | try { 116 | return self::cast($class, $key); 117 | } catch (ValueError) { 118 | return null; 119 | } 120 | } 121 | 122 | private static function match(UnitEnum|string $class, int|string $value): ?UnitEnum 123 | { 124 | $constants = self::cases($class); 125 | 126 | $case = self::findCase($constants, $value); 127 | 128 | if (!$case && is_a($class, BackedEnum::class, true)) { 129 | foreach ($constants as $constant) { 130 | if ($constant->value == $value) { 131 | $case = $constant; 132 | break; 133 | } 134 | } 135 | } 136 | 137 | if ($case) { 138 | return $case; 139 | } 140 | 141 | if (array_key_exists($value, array_keys($constants))) { 142 | return $constants[array_keys($constants)[$value]]; 143 | } 144 | 145 | return null; 146 | } 147 | 148 | protected static function throwError( 149 | UnitEnum|int|string|null $value, 150 | string $class 151 | ): never { 152 | throw new ValueError( 153 | sprintf( 154 | '"%s" is not a valid backing value for enum "%s"', 155 | is_object($value) ? $value->name : $value, 156 | $class 157 | ) 158 | ); 159 | } 160 | 161 | public static function cases( 162 | UnitEnum|string $class 163 | ): array { 164 | 165 | /** 166 | * @var class-string $class 167 | */ 168 | 169 | return array_filter( 170 | (new ReflectionClass($class))->getConstants(), 171 | fn($constant) => $constant instanceof $class 172 | ); 173 | } 174 | 175 | protected static function findCase(array $constants, int|string $value): ?UnitEnum 176 | { 177 | if (is_string($value)) { 178 | $value = strtolower($value); 179 | } 180 | 181 | foreach ($constants as $name => $case) { 182 | if (self::isCase($case, $name, $value)) { 183 | return $case; 184 | } 185 | } 186 | 187 | return null; 188 | } 189 | 190 | private static function isCase(mixed $case, string $name, int|string $value): bool 191 | { 192 | return $value === strtolower($name) 193 | || strtolower(backing($case) ?? '') === $value; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Helpers/EnumImplements.php: -------------------------------------------------------------------------------- 1 | [strtolower(basename(str_replace('\\', '/', $class))) => $class], 64 | $available 65 | ); 66 | 67 | return self::$available = array_merge(...$available); 68 | } 69 | 70 | public static function enumhancer(string $class): bool 71 | { 72 | if (self::implements($class, Enhancers::class)) { 73 | return true; 74 | } 75 | 76 | foreach (self::available() as $trait) { 77 | if (self::implements($class, $trait)) { 78 | return true; 79 | } 80 | } 81 | return false; 82 | } 83 | 84 | public static function __callStatic(string $name, array $arguments): bool 85 | { 86 | $implements = self::available()[strtolower($name)] ?? null; 87 | 88 | if ($implements && $arguments[0] && is_string($arguments[0])) { 89 | return self::implements($arguments[0], $implements); 90 | } 91 | 92 | throw new ErrorException( 93 | sprintf('Call to undefined method %s::%s()', self::class, $name), 94 | E_USER_ERROR 95 | ); 96 | } 97 | 98 | public static function implements(string $class, string $implements): bool 99 | { 100 | EnumCheck::check($class); 101 | 102 | return in_array($implements, self::classUsesTrait($class)); 103 | } 104 | 105 | private static function classUsesTrait(string $class): array 106 | { 107 | $results = []; 108 | 109 | foreach (array_reverse(class_parents($class) ?: []) + [$class => $class] as $class) { 110 | $results += self::getTraitsFrom($class); 111 | } 112 | 113 | return array_unique($results); 114 | } 115 | 116 | private static function getTraitsFrom(string $class): array 117 | { 118 | $traits = class_uses($class) ?: []; 119 | 120 | foreach ($traits as $class) { 121 | $traits += self::getTraitsFrom($class); 122 | } 123 | 124 | return $traits; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Helpers/EnumLabels.php: -------------------------------------------------------------------------------- 1 | name] 41 | ?? self::getLabels($enum::class)[EnumValue::key($enum)] 42 | ?? (method_exists($enum, 'value') ? $enum->value() : null) 43 | ?? $enum->value 44 | ?? $enum->name; 45 | } 46 | 47 | public static function getLabelOrName(UnitEnum $enum): string 48 | { 49 | return self::getLabels($enum::class)[$enum->name] ?? $enum->name; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Helpers/EnumMacros.php: -------------------------------------------------------------------------------- 1 | getMethods( 41 | ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED 42 | ); 43 | } 44 | 45 | /** 46 | * @throws ReflectionException 47 | */ 48 | public static function mixin(?string $enum, string|object $mixin): void 49 | { 50 | if (!is_null($enum)) { 51 | EnumCheck::check($enum); 52 | } 53 | 54 | if (is_string($mixin)) { 55 | $mixin = new $mixin(); 56 | } 57 | 58 | 59 | foreach (self::getMethodsFromMixin($mixin) as $method) { 60 | if ($enum) { 61 | self::macro($enum, $method->name, $method->invoke($mixin)); 62 | continue; 63 | } 64 | 65 | self::globalMacro($method->name, $method->invoke($mixin)); 66 | } 67 | 68 | unset($mixin); 69 | } 70 | 71 | public static function globalMixin(string|object $mixin): void 72 | { 73 | self::mixin(null, $mixin); 74 | } 75 | 76 | public static function flush(string $enum): void 77 | { 78 | EnumCheck::check($enum); 79 | 80 | if (isset(self::$macros[$enum])) { 81 | unset(self::$macros[$enum]); 82 | } 83 | } 84 | 85 | public static function flushGlobal(): void 86 | { 87 | self::$globalMacros = []; 88 | } 89 | 90 | public static function hasMacro(string $enum, string $name): bool 91 | { 92 | return self::getMacro($enum, $name) !== null; 93 | } 94 | 95 | private static function getMacro(string $enum, string $name): ?callable 96 | { 97 | EnumCheck::check($enum); 98 | 99 | $name = strtolower($name); 100 | 101 | return array_change_key_case(self::getMacros($enum))[$name] ?? null; 102 | } 103 | 104 | private static function getMacros(string $enum): array 105 | { 106 | return array_merge( 107 | self::$globalMacros ?? [], 108 | self::$macros[$enum] ?? [] 109 | ); 110 | } 111 | 112 | /** 113 | * @throws ReflectionException 114 | */ 115 | private static function isStaticMacro(callable $callable): bool 116 | { 117 | return (new ReflectionFunction($callable(...)))->isStatic(); 118 | } 119 | 120 | /** 121 | * @throws ReflectionException 122 | */ 123 | public static function call(UnitEnum $enum, string $name, array $arguments): mixed 124 | { 125 | EnumCheck::check($enum); 126 | 127 | $macro = self::getMacro($enum::class, $name); 128 | 129 | if ($macro && self::isStaticMacro($macro)) { 130 | return self::callStatic($enum::class, $name, $arguments); 131 | } 132 | 133 | $macro = ($macro ?? fn() => true)(...)->bindTo($enum, $enum::class); 134 | 135 | return $macro(...$arguments); 136 | } 137 | 138 | /** 139 | * @throws ReflectionException 140 | */ 141 | public static function callStatic(string $enum, string $name, array $arguments): mixed 142 | { 143 | EnumCheck::check($enum); 144 | 145 | $macro = self::getMacro($enum, $name); 146 | 147 | if (!$macro || false === self::isStaticMacro($macro)) { 148 | self::triggerError($enum, $name); 149 | } 150 | 151 | $macro = $macro(...)->bindTo(null, $enum); 152 | 153 | return $macro(...$arguments); 154 | } 155 | 156 | private static function triggerError(string $enum, string $name): never 157 | { 158 | throw new ErrorException( 159 | sprintf( 160 | 'Uncaught Error: Non-static method %s::%s() cannot be called statically', 161 | $enum, 162 | $name 163 | ), 164 | E_USER_ERROR 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Helpers/EnumMagicCalls.php: -------------------------------------------------------------------------------- 1 | '@default_configure', 15 | 'labels' => '@labels_configure', 16 | 'mapper' => '@mapper_configure', 17 | 'with_mapper' => '@with_mapper', 18 | 'state' => '@state_configure', 19 | 'hooks' => '@state_hook_configure' 20 | ]; 21 | 22 | private static array $global = []; 23 | protected static array $properties = []; 24 | protected static array $once = []; 25 | 26 | /** 27 | * @throws ReservedPropertyNameException|PropertyAlreadyStoredException 28 | */ 29 | public static function store(string $class, string $property, mixed $value, bool $allowReservedWord = false): void 30 | { 31 | EnumCheck::check($class); 32 | 33 | self::reservedWordCheck($property, $allowReservedWord); 34 | self::storedOnceCheck($class, $property); 35 | 36 | self::$properties[$class][$property] = $value; 37 | } 38 | 39 | /** 40 | * @throws ReservedPropertyNameException|PropertyAlreadyStoredException 41 | */ 42 | public static function storeOnce( 43 | string $class, 44 | string $property, 45 | mixed $value, 46 | bool $allowReservedWord = false 47 | ): void { 48 | EnumCheck::check($class); 49 | 50 | self::reservedWordCheck($property, $allowReservedWord); 51 | self::storedOnceCheck($class, $property); 52 | 53 | self::$once[$class][$property] = $value; 54 | unset(self::$properties[$class][$property]); 55 | } 56 | 57 | private static function reservedWordCheck(string $property, bool $allowReservedWord): void 58 | { 59 | if (!$allowReservedWord && in_array($property, self::$reserved)) { 60 | throw new ReservedPropertyNameException($property); 61 | } 62 | } 63 | 64 | private static function storedOnceCheck(string $class, string $property): void 65 | { 66 | if (isset(self::$once[$class][$property])) { 67 | throw new PropertyAlreadyStoredException($class, $property); 68 | } 69 | } 70 | 71 | public static function reservedWord(string $name): string 72 | { 73 | return self::$reserved[$name] ?? $name; 74 | } 75 | 76 | public static function get(string $class, string $property): mixed 77 | { 78 | EnumCheck::check($class); 79 | 80 | return self::$once[$class][$property] 81 | ?? self::$properties[$class][$property] 82 | ?? self::$global[$property] ?? null; 83 | } 84 | 85 | public static function getGlobal(string $property): mixed 86 | { 87 | return self::$global[$property] ?? null; 88 | } 89 | 90 | public static function global(string $property, mixed $value): mixed 91 | { 92 | return self::$global[$property] = $value; 93 | } 94 | 95 | public static function clear(string $class, string|null $property = null): void 96 | { 97 | EnumCheck::check($class); 98 | 99 | if (!empty($property)) { 100 | unset(self::$properties[$class][$property]); 101 | } 102 | 103 | if (empty($property)) { 104 | unset(self::$properties[$class]); 105 | } 106 | } 107 | 108 | public static function clearGlobal(string|null $property = null): void 109 | { 110 | if (!empty($property)) { 111 | unset(self::$global[$property]); 112 | } 113 | 114 | if (empty($property)) { 115 | self::$global = []; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Helpers/EnumProxy.php: -------------------------------------------------------------------------------- 1 | name = $enum->name; 16 | $this->value = (string)EnumValue::value($enum, $keepValueCase); 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | return $this->value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Helpers/EnumReporter.php: -------------------------------------------------------------------------------- 1 | name; 70 | } 71 | 72 | if (!is_null($key)) { 73 | $key = (string)$key; 74 | } 75 | 76 | $reporter?->report($class, $key, $context); 77 | } 78 | 79 | return $enum; 80 | } 81 | 82 | /** 83 | * @param string $class 84 | * @param iterable $keys 85 | * @param BackedEnum|null $context 86 | * @param Reporter|null $reporter 87 | * @return UnitEnum[] 88 | */ 89 | public static function getOrReportArray( 90 | string $class, 91 | iterable $keys, 92 | ?BackedEnum $context, 93 | ?Reporter $reporter 94 | ): array { 95 | EnumCheck::check($class); 96 | 97 | $result = []; 98 | 99 | foreach ($keys as $key) { 100 | $result[] = self::getOrReport($class, $key, $context, $reporter); 101 | } 102 | 103 | return array_filter($result); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Helpers/EnumState.php: -------------------------------------------------------------------------------- 1 | name] = $case; 29 | } 30 | 31 | $current = $case; 32 | } 33 | unset($current); 34 | 35 | return array_merge( 36 | $transitions, 37 | self::castTransitions($class, $userTransitions) 38 | ); 39 | } 40 | 41 | public static function allowedTransitions(UnitEnum $currentTransition, ?TransitionHook ...$hooks): array 42 | { 43 | return self::filterAllowedTransitions($currentTransition, self::getTransitions($currentTransition), $hooks); 44 | } 45 | 46 | private static function getTransitions(UnitEnum $currentTransition): array 47 | { 48 | $transitions = array_change_key_case( 49 | EnumState::transitions($currentTransition::class, $currentTransition::class::transitions()) 50 | ); 51 | 52 | $transitions = $transitions[$currentTransition->name] 53 | ?? $transitions[strtolower($currentTransition->name)] 54 | ?? $transitions[EnumValue::value($currentTransition)] 55 | ?? []; 56 | 57 | return array_filter(is_array($transitions) ? $transitions : [$transitions]); 58 | } 59 | 60 | private static function castTransitions(string|UnitEnum $class, array $transitions): array 61 | { 62 | foreach ($transitions as $key => $value) { 63 | unset($transitions[$key]); 64 | 65 | $key = EnumGetters::tryCast($class, $key)->name ?? $key; 66 | 67 | if (is_array($value)) { 68 | $transitions[$key] = self::castTransitions($class, $value); 69 | continue; 70 | } 71 | 72 | $transitions[$key] = $value ? EnumGetters::cast($class, $value) : null; 73 | } 74 | 75 | return $transitions; 76 | } 77 | 78 | private static function filterAllowedTransitions( 79 | UnitEnum $currentTransition, 80 | array $transitions, 81 | array $hooks 82 | ): array { 83 | return array_filter( 84 | $transitions, 85 | function (UnitEnum $transitionTo) use ($hooks, $currentTransition) { 86 | foreach ($hooks as $hook) { 87 | if (!$hook->isAllowed($currentTransition, $transitionTo)) { 88 | return false; 89 | } 90 | } 91 | return true; 92 | } 93 | ); 94 | } 95 | 96 | public static function isValidCall(string $class, string $name): bool 97 | { 98 | EnumCheck::check($class); 99 | 100 | return (str_starts_with($name, 'to') || str_starts_with($name, 'tryTo')) 101 | && self::getToState($class, $name) !== null; 102 | } 103 | 104 | private static function getToState(string $class, string $name): ?UnitEnum 105 | { 106 | return EnumGetters::tryGet( 107 | $class, 108 | substr($name, str_starts_with($name, 'tryTo') ? 5 : 2), 109 | true, 110 | false 111 | ); 112 | } 113 | 114 | public static function call(UnitEnum $currentState, string $name, array $arguments): mixed 115 | { 116 | $toState = self::getToState($currentState::class, $name); 117 | 118 | /** 119 | * @var State|UnitEnum $currentState 120 | */ 121 | if (str_starts_with($name, 'tryTo')) { 122 | return $currentState->tryTo($toState, ...$arguments); 123 | } 124 | 125 | return $currentState->to($toState, ...$arguments); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Helpers/EnumValue.php: -------------------------------------------------------------------------------- 1 | value ?? ($keepCase ? $enum->name : strtolower($enum->name)); 20 | } 21 | 22 | public static function key(UnitEnum $enum): int 23 | { 24 | if (property_exists($enum, 'value') && is_numeric($enum->value)) { 25 | return (int)$enum->value; 26 | } 27 | 28 | return (int)array_search($enum, $enum::cases()); 29 | } 30 | 31 | private static function isStrict(UnitEnum $enum): bool 32 | { 33 | $constants = (new ReflectionClass($enum))->getConstants(); 34 | 35 | foreach ($constants as $name => $constant) { 36 | if ('strict' === strtolower($name) && is_bool($constant)) { 37 | return $constant; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Helpers/Enumhancer.php: -------------------------------------------------------------------------------- 1 | > $mappable 12 | */ 13 | public function __construct(private readonly array $mappable) 14 | { 15 | } 16 | 17 | 18 | protected function mappable(): array 19 | { 20 | return $this->mappable; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Helpers/Mappers/EnumMapper.php: -------------------------------------------------------------------------------- 1 | |Mapper|string|null ...$mappers 27 | * @return string|null 28 | */ 29 | public static function map( 30 | string $enum, 31 | UnitEnum|int|string|null $value, 32 | array|string|Mapper|null ...$mappers 33 | ): ?string { 34 | EnumCheck::check($enum); 35 | 36 | if (null === $value) { 37 | return null; 38 | } 39 | 40 | foreach (self::getMappers($enum, ...$mappers) as $mapper) { 41 | $value = $mapper->map($value, $enum) ?? $value; 42 | } 43 | 44 | return $value instanceof UnitEnum ? $value->name : $value; 45 | } 46 | 47 | /** 48 | * @param string $enum 49 | * @param iterable $values 50 | * @param Mapper|string|array>|null ...$mappers 51 | * @return string[] 52 | */ 53 | public static function mapArray(string $enum, iterable $values, Mapper|string|array|null ...$mappers): array 54 | { 55 | $mapped = []; 56 | 57 | foreach ($values as $value) { 58 | $mapped[] = EnumMapper::map($enum, $value, ...$mappers); 59 | } 60 | 61 | return $mapped; 62 | } 63 | 64 | /** 65 | * @return Mapper[] 66 | */ 67 | private static function sanitizeMapperArray(array $mappers): array 68 | { 69 | return array_map( 70 | function (Mapper|array|string $mapper) { 71 | 72 | if (is_string($mapper)) { 73 | $mapper = new $mapper; 74 | } 75 | 76 | if (is_array($mapper)) { 77 | $mapper = self::wrapInMapper($mapper); 78 | } 79 | 80 | if (!$mapper instanceof Mapper) { 81 | throw new RuntimeException( 82 | sprintf( 83 | 'object of type \'%s\' expected, got \'%s\'', 84 | Mapper::class, 85 | $mapper::class 86 | ) 87 | ); 88 | } 89 | return $mapper; 90 | }, 91 | array_filter($mappers) 92 | ); 93 | } 94 | 95 | /** 96 | * @param string $enum 97 | * @return Mapper[] 98 | * @throws ReflectionException 99 | */ 100 | private static function getConstantMappers(string $enum): array 101 | { 102 | /** 103 | * @var UnitEnum $enum 104 | */ 105 | 106 | $mappers = []; 107 | $constants = (new ReflectionClass($enum))->getConstants(); 108 | 109 | foreach ($constants as $name => $constant) { 110 | if (!str_starts_with(strtolower($name), 'map')) { 111 | continue; 112 | } 113 | 114 | $mappers[] = self::parseConstantAsMapper($enum, $name, $constant); 115 | } 116 | 117 | return $mappers; 118 | } 119 | 120 | protected static function parseConstantAsMapper(UnitEnum|string $enum, string $name, mixed $constant): ?Mapper 121 | { 122 | if (is_array($constant)) { 123 | return self::wrapInMapper($constant); 124 | } 125 | 126 | if (!is_string($constant) || !class_exists($constant)) { 127 | return null; 128 | } 129 | 130 | self::checkMappers(is_object($enum) ? $enum::class : $enum, $constant); 131 | 132 | return self::instantiateMapper( 133 | $constant, 134 | str_starts_with( 135 | strtolower($name), 136 | 'map_flip' 137 | ) 138 | ); 139 | } 140 | 141 | public static function isValidMapper(string $enum, mixed $value): bool 142 | { 143 | return self::isMapperClass($value) 144 | || is_array($value) 145 | || is_a($value, $enum); 146 | } 147 | 148 | public static function isMapperClass(mixed $mapper): bool 149 | { 150 | return is_subclass_of($mapper, Mapper::class); 151 | } 152 | 153 | protected static function instantiateMapper(string $class, bool $flip = false): Mapper 154 | { 155 | /** 156 | * @var $class Mapper 157 | */ 158 | if ($flip) { 159 | return $class::flip(); 160 | } 161 | 162 | return $class::newInstance(); 163 | } 164 | 165 | /** 166 | * @param array> $map 167 | * @return Mapper 168 | */ 169 | protected static function wrapInMapper(array $map): Mapper 170 | { 171 | return EnumArrayMapper::newInstance($map); 172 | } 173 | 174 | /** 175 | * @param string $enum 176 | * @return array> 177 | */ 178 | private static function getConfiguredMapper(string $enum): array 179 | { 180 | return EnumProperties::get( 181 | $enum, 182 | EnumProperties::reservedWord('mapper') 183 | ) ?? []; 184 | } 185 | 186 | /** 187 | * @param string $enum 188 | * @param Mapper|string|array|null ...$mappers 189 | * @return Mapper 190 | * @throws ReflectionException 191 | */ 192 | public static function getMappers(string $enum, Mapper|string|array|null ...$mappers): array 193 | { 194 | /** 195 | * @var $enum Mappers|UnitEnum|String 196 | */ 197 | return self::sanitizeMapperArray( 198 | [ 199 | ...$mappers, 200 | ...self::getConfiguredMapper($enum), 201 | ...self::getConstantMappers($enum) 202 | ] 203 | ); 204 | } 205 | 206 | public static function checkMappers(string $enum, mixed ...$mappers): void 207 | { 208 | EnumCheck::check($enum); 209 | 210 | array_walk( 211 | $mappers, 212 | function ($mapper) { 213 | if (is_string($mapper) && !EnumMapper::isMapperClass($mapper)) { 214 | throw new ValueError( 215 | sprintf('Invalid class. expected Mapper, %s given', $mapper) 216 | ); 217 | } 218 | } 219 | ); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/Helpers/Subset/EnumSubsetMethods.php: -------------------------------------------------------------------------------- 1 | $enumType 24 | * @param T ...$enums 25 | */ 26 | public function __construct(private readonly string $enumType, UnitEnum ...$enums) 27 | { 28 | EnumCheck::matches($enumType, ...$enums); 29 | 30 | $this->enums = $enums; 31 | } 32 | 33 | public function do(Closure $callable): void 34 | { 35 | foreach ($this->enums as $enum) { 36 | $callable($enum); 37 | } 38 | } 39 | 40 | public function equals(UnitEnum|string|int|null ...$equals): bool 41 | { 42 | foreach ($this->enums as $enum) { 43 | if ($this->compare($enum, ...$equals)) { 44 | return true; 45 | } 46 | } 47 | 48 | return false; 49 | } 50 | 51 | private function compare(UnitEnum $enum, UnitEnum|string|int|null ...$equals): bool 52 | { 53 | $result = false; 54 | foreach ($equals as $equal) { 55 | $equal = $this->asEnumObject($equal); 56 | 57 | EnumCheck::matches($this->enumType, $equal); 58 | 59 | if ($enum === $equal) { 60 | $result = true; 61 | } 62 | } 63 | return $result; 64 | } 65 | 66 | private function asEnumObject(mixed $value): ?UnitEnum 67 | { 68 | if (!$value instanceof UnitEnum || $this->enumType !== $value::class) { 69 | return EnumGetters::tryGet($this->enumType, $value, true); 70 | } 71 | 72 | return $value; 73 | } 74 | 75 | 76 | /** 77 | * @return string[] 78 | */ 79 | public function names(): array 80 | { 81 | return array_map(fn(UnitEnum $enum) => $enum->name, $this->enums); 82 | } 83 | 84 | /** 85 | * @return string[]|int[] 86 | */ 87 | public function values(bool|null $keepEnumCase = null): array 88 | { 89 | return array_map( 90 | function (mixed $enum) use ($keepEnumCase) { 91 | return EnumValue::value($enum, $keepEnumCase); 92 | }, 93 | $this->enums 94 | ); 95 | } 96 | 97 | /** 98 | * @param bool|null $keepEnumCase 99 | * @return array 100 | */ 101 | public function dropdown(bool|null $keepEnumCase = null): array 102 | { 103 | return array_replace( 104 | [], 105 | ...array_map( 106 | function (UnitEnum $case) use ($keepEnumCase) { 107 | 108 | return [ 109 | EnumValue::value($case, $keepEnumCase) 110 | => 111 | EnumLabels::getLabelOrName($case) 112 | ]; 113 | }, 114 | $this->enums 115 | ) 116 | ); 117 | } 118 | 119 | /** 120 | * @return T[] 121 | */ 122 | public function cases(): array 123 | { 124 | return $this->enums; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Laravel/Concerns/CastsBasicEnumerations.php: -------------------------------------------------------------------------------- 1 | getCasts()[$key]; 26 | 27 | if (!$value instanceof $castType) { 28 | $value = EnumGetters::get($castType, $value); 29 | } 30 | 31 | if ($this->shouldUseBasicEnumWorkaround($castType)) { 32 | $keepEnumCase = property_exists($this, 'keepEnumCase') ? $this->keepEnumCase : true; 33 | 34 | return backing($value, $keepEnumCase); 35 | } 36 | 37 | return $value; 38 | } 39 | 40 | protected function setEnumCastableAttribute($key, $value) 41 | { 42 | $enumClass = $this->getCasts()[$key]; 43 | 44 | $keepEnumCase = property_exists($this, 'keepEnumCase') ? $this->keepEnumCase : true; 45 | 46 | if (!isset($value)) { 47 | $this->attributes[$key] = null; 48 | return; 49 | } 50 | 51 | if ($value instanceof $enumClass) { 52 | $value = EnumValue::value($value, $keepEnumCase); 53 | } 54 | 55 | if ($value instanceof UnitEnum && !$value instanceof $enumClass) { 56 | throw new ValueError( 57 | sprintf('Enum of `%s` expected, got `%s`', $enumClass, $value::class) 58 | ); 59 | } 60 | 61 | $this->attributes[$key] = EnumValue::value(EnumGetters::get($enumClass, $value), $keepEnumCase); 62 | } 63 | 64 | private function shouldUseBasicEnumWorkaround(string $enumClass): bool 65 | { 66 | return (!is_subclass_of($enumClass, BackedEnum::class, true)) 67 | && 'toArray' === (debug_backtrace(2)[5]['function'] ?? null); 68 | } 69 | 70 | protected function getStorableEnumValue($expectedEnumValue, $value = null) 71 | { 72 | $value ??= $expectedEnumValue; 73 | if ($value instanceof UnitEnum) { 74 | $keepEnumCase = property_exists($this, 'keepEnumCase') ? $this->keepEnumCase : true; 75 | 76 | return EnumValue::value($value, $keepEnumCase); 77 | } 78 | 79 | return $value; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Laravel/Concerns/CastsStatefulEnumerations.php: -------------------------------------------------------------------------------- 1 | getAttribute($key); 31 | 32 | if (!isset($value) || !$currentAttribute || $this->shouldNotCastByTransition($key)) { 33 | $this->setEnumCastableAttributeAnyway($key, $value); 34 | return; 35 | } 36 | 37 | $this->setEnumCastableAttributeAnyway( 38 | $key, 39 | $currentAttribute->transitionTo($value, $this->getTransactionHooks($key)) 40 | ); 41 | } 42 | 43 | private function shouldNotCastByTransition($key): bool 44 | { 45 | $cast = $this->getCasts()[$key]; 46 | 47 | $ignore = property_exists($this, 'castsIgnoreEnumState') ? $this->castsIgnoreEnumState : []; 48 | 49 | return in_array($key, $ignore) || !EnumImplements::state($cast); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Laravel/Middleware/SubstituteEnums.php: -------------------------------------------------------------------------------- 1 | getParameters($request->route()) as $key => $parameter) { 23 | if ($request->route($key) === null) { 24 | $this->setDefaultIfAvailable($request, $key, $parameter); 25 | continue; 26 | } 27 | 28 | $this->processParameter($parameter, $request, $key); 29 | } 30 | 31 | return $next($request); 32 | } 33 | 34 | protected function processParameter(ReflectionEnum $parameter, Request $request, string $key): void 35 | { 36 | /** 37 | * @var string $givenValue 38 | */ 39 | $givenValue = $request->route($key); 40 | 41 | $value = EnumGetters::tryGet($parameter->getName(), $givenValue, useDefault: false); 42 | 43 | if (!$value) { 44 | throw new NotFoundHttpException(); 45 | } 46 | 47 | /** 48 | * Laravel's middleware SubstituteBindings is still being processed. Returning the value allows 49 | * that middleware to complete the request properly. 50 | */ 51 | if ($this->isStringBacked($parameter)) { 52 | /** 53 | * @var BackedEnum $value 54 | */ 55 | $value = $value->value; 56 | } 57 | 58 | $request->route()?->setParameter($key, $value); 59 | } 60 | 61 | /** 62 | * @param Route|null $route 63 | * @return array 64 | * @throws ReflectionException 65 | */ 66 | private function getParameters(Route|null $route): array 67 | { 68 | return collect( 69 | $route?->signatureParameters(['subClass' => UnitEnum::class]) 70 | )->mapWithKeys( 71 | function (ReflectionParameter $parameter) { 72 | $backedEnumClass = ltrim((string)$parameter->getType(), '?'); 73 | 74 | return [$parameter->getName() => new ReflectionEnum($backedEnumClass)]; 75 | } 76 | )->filter()->toArray(); 77 | } 78 | 79 | private function isStringBacked(ReflectionEnum $parameter): bool 80 | { 81 | return ((string)$parameter->getBackingType()) === 'string'; 82 | } 83 | 84 | private function hasDefault(ReflectionEnum $parameter): bool 85 | { 86 | return EnumImplements::defaults($parameter->getName()); 87 | } 88 | 89 | private function setDefaultIfAvailable( 90 | Request $request, 91 | string $key, 92 | ReflectionEnum $parameter 93 | ): void { 94 | if ($this->hasDefault($parameter)) { 95 | $request->route()?->setParameter( 96 | $key, 97 | EnumDefaults::default($parameter->getName()) 98 | ); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Laravel/Mixins/FormRequestMixin.php: -------------------------------------------------------------------------------- 1 | get($key), ...$mappers) 28 | ); 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Laravel/Mixins/RulesMixin.php: -------------------------------------------------------------------------------- 1 | setupReporter(); 20 | 21 | $this->setupMacroMixins(); 22 | 23 | $this->setupEnumBindingMiddleware($kernel); 24 | } 25 | 26 | protected function setupReporter(): void 27 | { 28 | LogLevel::setDefault(LogLevel::Notice); 29 | 30 | EnumReporter::laravel(); 31 | } 32 | 33 | private function setupMacroMixins(): void 34 | { 35 | Rule::mixin(new RulesMixin()); 36 | FormRequest::mixin(new FormRequestMixin()); 37 | } 38 | 39 | protected function setupEnumBindingMiddleware(Kernel $kernel): void 40 | { 41 | /** 42 | * @var \Illuminate\Foundation\Http\Kernel $kernel 43 | */ 44 | $kernel->prependToMiddlewarePriority(SubstituteEnums::class); 45 | $kernel->appendMiddlewareToGroup('web', SubstituteEnums::class); 46 | $kernel->appendMiddlewareToGroup('api', SubstituteEnums::class); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Laravel/Reporters/LaravelLogReporter.php: -------------------------------------------------------------------------------- 1 | level = $level; 25 | $this->channels = $channels; 26 | } 27 | 28 | private function getLevel(): string 29 | { 30 | return (string)($this->level ?? LogLevel::default() ?? LogLevel::Notice)->value(); 31 | } 32 | 33 | /** 34 | * @return string[] 35 | */ 36 | private function getChannels(): array 37 | { 38 | if (empty($this->channels)) { 39 | return [config('logging.default')]; 40 | } 41 | 42 | return $this->channels; 43 | } 44 | 45 | public function report(string $enum, ?string $key, ?BackedEnum $context): void 46 | { 47 | Log::stack( 48 | $this->getChannels() 49 | )->log( 50 | $this->getLevel(), 51 | class_basename($enum) 52 | . ($key ? sprintf(' does not have \'%s\'', $key) : ': A null value was passed'), 53 | array_filter( 54 | [ 55 | 'class' => $enum, 56 | 'key' => $key, 57 | 'context' => $context?->value 58 | ] 59 | ) 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Laravel/Rules/EnumBitmask.php: -------------------------------------------------------------------------------- 1 | value = $value; 32 | 33 | if ($this->singleBit) { 34 | return $value == 0 35 | || (EnumBitmasks::isBit($value) 36 | && array_key_exists($value, EnumBitmasks::getCaseBits($this->type)) 37 | ); 38 | } 39 | 40 | return EnumBitmasks::isValidBitmask($this->type, $value); 41 | } 42 | 43 | /** 44 | * @return string|string[] 45 | */ 46 | public function message(): string|array 47 | { 48 | $message = trans( 49 | 'validation.enumhancer.bitmask', 50 | [ 51 | 'enum' => $this->type, 52 | 'value' => $this->value, 53 | ] 54 | ); 55 | 56 | return $message === 'validation.enumhancer.bitmask' 57 | ? ['The selected :attribute is invalid.'] 58 | : $message; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Laravel/Rules/EnumTransition.php: -------------------------------------------------------------------------------- 1 | currentState::class)) { 23 | throw new ErrorException( 24 | sprintf('%s does not implement `State`', $this->currentState::class), 25 | E_USER_ERROR 26 | ); 27 | } 28 | } 29 | 30 | /** 31 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 32 | */ 33 | public function passes($attribute, $value) 34 | { 35 | $this->transitionTo = $value; 36 | 37 | /** 38 | * @var State $currentState 39 | */ 40 | $currentState = $this->currentState; 41 | 42 | return $currentState->isTransitionAllowed($value, $this->hook); 43 | } 44 | 45 | /** 46 | * Get the validation error message. 47 | * 48 | * @return string[] 49 | */ 50 | public function message() 51 | { 52 | $message = trans('validation.enumhancer.transition', [ 53 | 'from' => $this->currentState->name, 54 | 'to' => $this->transitionTo ?? 'unknown', 55 | ]); 56 | 57 | return $message === 'validation.enumhancer.transition' 58 | ? ['The transition for :attribute is invalid.'] 59 | : $message; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Laravel/Rules/IsEnum.php: -------------------------------------------------------------------------------- 1 | |Mapper[]|null[]|string[] 18 | */ 19 | private array $mappers; 20 | 21 | /** 22 | * @param string $type 23 | * @param array|array|Mapper[]|null[]|string[] $mappers 24 | */ 25 | public function __construct(private readonly string $type, Mapper|string|array|null ...$mappers) 26 | { 27 | EnumCheck::check($type); 28 | $this->mappers = $mappers; 29 | } 30 | 31 | /** 32 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) 33 | */ 34 | public function passes($attribute, $value): bool 35 | { 36 | $this->value = EnumMapper::map($this->type, $value, ...$this->mappers); 37 | 38 | return (bool)EnumGetters::tryGet($this->type, $this->value, useDefault: false); 39 | } 40 | 41 | /** 42 | * @return string|string[] 43 | */ 44 | public function message(): string|array 45 | { 46 | $message = trans( 47 | 'validation.enumhancer.enum', 48 | [ 49 | 'enum' => $this->type, 50 | 'value' => $this->value, 51 | ] 52 | ); 53 | 54 | return $message === 'validation.enumhancer.enum' 55 | ? ['The selected :attribute is invalid.'] 56 | : $message; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/PHPStan/Constants/BitmaskConstantAlwaysUsed.php: -------------------------------------------------------------------------------- 1 | getName()) !== 'bit_values') { 15 | return false; 16 | } 17 | 18 | $class = $constant->getDeclaringClass(); 19 | 20 | if (!$class->isEnum()) { 21 | return false; 22 | } 23 | 24 | return EnumImplements::bitmasks($class->getName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/PHPStan/Constants/BitmaskModifierConstantAlwaysUsed.php: -------------------------------------------------------------------------------- 1 | getName()) !== 'bit_modifier') { 16 | return false; 17 | } 18 | 19 | $class = $constant->getDeclaringClass(); 20 | 21 | if (!$class->hasConstant('BIT_VALUES')) { 22 | return false; 23 | } 24 | 25 | if (!$class->getBackedEnumType()?->isInteger()) { 26 | return false; 27 | } 28 | 29 | 30 | return EnumImplements::bitmasks($class->getName()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/PHPStan/Constants/DefaultConstantAlwaysUsed.php: -------------------------------------------------------------------------------- 1 | getName(); 14 | 15 | if (\strtolower($constantName) === 'default') { 16 | $class = $constant->getDeclaringClass(); 17 | return $class->isEnum() && EnumImplements::defaults($class->getName()); 18 | } 19 | 20 | return false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/PHPStan/Constants/MapperConstantAlwaysUsed.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass(); 16 | 17 | if (!$class->isEnum()) { 18 | return false; 19 | } 20 | 21 | $className = $class->getName(); 22 | $constantName = $constant->getName(); 23 | 24 | if (str_starts_with(strtolower($constantName), 'map')) { 25 | return EnumImplements::mappers($className); 26 | } 27 | 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/PHPStan/Constants/Rules/DefaultConstantRule.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class DefaultConstantRule implements Rule 19 | { 20 | public function getNodeType(): string 21 | { 22 | return ClassConst::class; 23 | } 24 | 25 | public function processNode(Node $node, Scope $scope): array 26 | { 27 | $constantName = $node->consts[0]->name->name; 28 | 29 | if ($this->isDefault($constantName)) { 30 | return []; 31 | } 32 | 33 | $reflectedClass = $scope->getClassReflection(); 34 | 35 | if (!$reflectedClass->isEnum()) { 36 | return []; 37 | } 38 | 39 | return $this->validate($reflectedClass, $constantName); 40 | } 41 | 42 | /** 43 | * @param ClassReflection|null $reflectedClass 44 | * @param string $constantName 45 | * @return string[] 46 | * @throws ReflectionException 47 | */ 48 | protected function validate(?ClassReflection $reflectedClass, string $constantName): array 49 | { 50 | $implementsDefault = EnumImplements::defaults($reflectedClass->getName()); 51 | 52 | $value = $reflectedClass->getConstant($constantName)->getValueType(); 53 | 54 | $valueFromEnum = $value instanceof EnumCaseObjectType && $value->getClassName() === $reflectedClass->getName(); 55 | 56 | $return = []; 57 | 58 | if ($implementsDefault && !$valueFromEnum) { 59 | $return = [ 60 | RuleErrorBuilder::message( 61 | sprintf( 62 | 'Enumhancer: enum is implementing `Defaults`, ' 63 | . 'but constant `%s` is not referencing to one of its own cases.', 64 | $constantName 65 | ) 66 | )->build() 67 | ]; 68 | } 69 | 70 | if (!$implementsDefault && $valueFromEnum) { 71 | $return = [ 72 | RuleErrorBuilder::message( 73 | sprintf( 74 | 'Enumhancer: Constant `%s` is not going to be used, ' 75 | . 'because enum is not implementing `Defaults`', 76 | $constantName 77 | ) 78 | )->build() 79 | ]; 80 | } 81 | 82 | return $return; 83 | } 84 | 85 | protected function isDefault(string $constantName): bool 86 | { 87 | return strtolower($constantName) !== 'default'; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/PHPStan/Constants/Rules/MapperConstantRule.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class MapperConstantRule implements Rule 23 | { 24 | public function __construct( 25 | private ReflectionProvider $reflectionProvider 26 | ) { 27 | } 28 | 29 | public function getNodeType(): string 30 | { 31 | return ClassConst::class; 32 | } 33 | 34 | public function processNode(Node $node, Scope $scope): array 35 | { 36 | $constantName = $node->consts[0]->name->name; 37 | 38 | if (!$this->isMapperConstant($constantName)) { 39 | return []; 40 | } 41 | 42 | $classReflection = $scope->getClassReflection(); 43 | 44 | if (!$classReflection->isEnum()) { 45 | return []; 46 | } 47 | 48 | return $this->validate($classReflection, $constantName); 49 | } 50 | 51 | private function isMapperConstant(string $name): bool 52 | { 53 | return str_starts_with(strtolower($name), 'map'); 54 | } 55 | 56 | /** 57 | * @param ClassReflection|null $class 58 | * @param string $constantName 59 | * @return array 60 | * @throws MissingConstantFromReflectionException 61 | */ 62 | protected function validate(?ClassReflection $class, string $constantName): array 63 | { 64 | $implementsMappers = EnumImplements::mappers($class->getName()); 65 | $return = []; 66 | 67 | $isValid = $this->isValidValue($class, $constantName); 68 | 69 | if (!$implementsMappers && $isValid) { 70 | $return = [ 71 | RuleErrorBuilder::message( 72 | sprintf( 73 | 'Enumhancer: `%s` is not going to be used because enum is not implementing `Mappers`', 74 | $constantName, 75 | ) 76 | )->build() 77 | ]; 78 | } 79 | 80 | if ($implementsMappers && !$isValid) { 81 | $return = [ 82 | RuleErrorBuilder::message( 83 | sprintf( 84 | 'Enumhancer: The specified `%s` map is invalid', 85 | $constantName, 86 | ) 87 | )->build() 88 | ]; 89 | } 90 | 91 | return $return; 92 | } 93 | 94 | /** 95 | * @throws MissingConstantFromReflectionException 96 | */ 97 | protected function isValidValue( 98 | ?ClassReflection $class, 99 | string $constantName 100 | ): bool { 101 | $valueType = $class->getConstant($constantName)->getValueType(); 102 | 103 | $isValid = $valueType instanceof ConstantArrayType; 104 | 105 | if ($valueType instanceof ConstantStringType) { 106 | $class = $valueType->getValue(); 107 | try { 108 | $classReflection = $this->reflectionProvider->getClass($class); 109 | $isValid = $classReflection->isSubclassOf(Mapper::class); 110 | } catch (\PHPStan\Broker\ClassNotFoundException) { 111 | $isValid = false; 112 | } 113 | } 114 | 115 | return $isValid; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/PHPStan/Constants/Rules/StrictConstantRule.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class StrictConstantRule implements Rule 17 | { 18 | public function getNodeType(): string 19 | { 20 | return ClassConstantsNode::class; 21 | } 22 | 23 | public function processNode(Node $node, Scope $scope): array 24 | { 25 | $constantName = $node->getConstants()[0]->consts[0]->name->name; 26 | 27 | if (strtolower($constantName) !== 'strict') { 28 | return []; 29 | } 30 | 31 | $class = $scope->getClassReflection(); 32 | 33 | if ($this->shouldProcessEnum($class)) { 34 | return []; 35 | } 36 | 37 | if ($class->getConstant($constantName)->getValueType()->isBoolean()->no()) { 38 | return [ 39 | RuleErrorBuilder::message( 40 | sprintf('Enumhancer: constant `%s` should be a boolean.', $constantName) 41 | )->build() 42 | ]; 43 | } 44 | 45 | return []; 46 | } 47 | 48 | /** 49 | * @param ClassReflection|null $class 50 | * @return bool 51 | */ 52 | protected function shouldProcessEnum(?ClassReflection $class): bool 53 | { 54 | return !$class->isEnum() || !EnumImplements::enumhancer($class->getName()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/PHPStan/Constants/StrictConstantAlwaysUsed.php: -------------------------------------------------------------------------------- 1 | getName()) !== 'strict') { 15 | return false; 16 | } 17 | 18 | $class = $constant->getDeclaringClass(); 19 | 20 | if (!$class->isEnum()) { 21 | return false; 22 | } 23 | 24 | return EnumImplements::enumhancer($class->getName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/PHPStan/Methods/EnumComparisonMethodsClassReflection.php: -------------------------------------------------------------------------------- 1 | isEnum()) { 18 | return false; 19 | } 20 | 21 | return EnumCompare::isValidCall( 22 | $classReflection->getName(), 23 | $methodName, 24 | [] 25 | ); 26 | } 27 | 28 | public function getMethod( 29 | ClassReflection $classReflection, 30 | string $methodName 31 | ): MethodReflection { 32 | return new ClosureMethodReflection( 33 | $classReflection, 34 | $methodName, 35 | new ClosureType([], new BooleanType(), false) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/PHPStan/Methods/EnumConstructorMethodsClassReflection.php: -------------------------------------------------------------------------------- 1 | isEnum()) { 19 | return false; 20 | } 21 | 22 | $className = $classReflection->getName(); 23 | 24 | if (!EnumImplements::constructor($className)) { 25 | return false; 26 | } 27 | 28 | return EnumGetters::tryGet($className, $methodName, useDefault: false) !== null; 29 | } 30 | 31 | public function getMethod( 32 | ClassReflection $classReflection, 33 | string $methodName 34 | ): MethodReflection { 35 | 36 | return new ClosureMethodReflection( 37 | $classReflection, 38 | $methodName, 39 | new ClosureType( 40 | [], 41 | new ObjectType($classReflection->getName()), 42 | false 43 | ), 44 | true 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/PHPStan/Methods/EnumMacrosMethodsClassReflection.php: -------------------------------------------------------------------------------- 1 | isEnum()) { 26 | return false; 27 | } 28 | 29 | $className = $classReflection->getName(); 30 | 31 | if (EnumImplements::macros($className)) { 32 | return $this->getMacro($className, $methodName) !== null; 33 | } 34 | 35 | return false; 36 | } 37 | 38 | public function getMethod( 39 | ClassReflection $classReflection, 40 | string $methodName 41 | ): MethodReflection { 42 | $className = $classReflection->getName(); 43 | $macro = $this->getMacro($className, $methodName); 44 | /** 45 | * PHPStan does not support isStatic on closureType 46 | * @phpstan-ignore-next-line 47 | */ 48 | $nativeReflection = new ReflectionFunction($macro); 49 | 50 | try { 51 | return (new ClosureMethodReflection( 52 | $classReflection, 53 | $methodName, 54 | $this->closureTypeFactory->fromClosureObject( 55 | $macro 56 | ), 57 | $nativeReflection->isStatic(), 58 | ))->setDocDocument( 59 | $nativeReflection->getDocComment() 60 | ); 61 | } catch (LogicException) { 62 | /** 63 | * Transforming Illogic Exception into an explanatory Logically clear exception. 64 | */ 65 | throw new LogicException( 66 | sprintf( 67 | 'PHPStan Could not properly parse closure `%s` for `%s`, ' 68 | . 'Check if a default value\'s code is not trying to execute this macro.', 69 | $methodName, 70 | $className 71 | ) 72 | ); 73 | } 74 | } 75 | 76 | public function getMacro(string $class, string $macro): ?Closure 77 | { 78 | return Closure::bind( 79 | function (string $class, string $macro) { 80 | return EnumMacros::getMacro($class, $macro); 81 | }, 82 | null, 83 | EnumMacros::class 84 | )($class, $macro); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/PHPStan/Methods/EnumStateMethodsClassReflection.php: -------------------------------------------------------------------------------- 1 | isEnum()) { 18 | return false; 19 | } 20 | 21 | return EnumState::isValidCall( 22 | $classReflection->getName(), 23 | $methodName 24 | ); 25 | } 26 | 27 | public function getMethod( 28 | ClassReflection $classReflection, 29 | string $methodName 30 | ): MethodReflection { 31 | 32 | return new ClosureMethodReflection( 33 | $classReflection, 34 | $methodName, 35 | new ClosureType( 36 | [], 37 | new ObjectType($classReflection->getName()), 38 | false 39 | ) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/PHPStan/Reflections/ClosureMethodReflection.php: -------------------------------------------------------------------------------- 1 | docComment = $docComment; 30 | return $this; 31 | } 32 | 33 | 34 | public function getDeclaringClass(): ClassReflection 35 | { 36 | return $this->classReflection; 37 | } 38 | 39 | public function isPrivate(): bool 40 | { 41 | return false; 42 | } 43 | 44 | public function isPublic(): bool 45 | { 46 | return true; 47 | } 48 | 49 | public function isFinal(): TrinaryLogic 50 | { 51 | return TrinaryLogic::createNo(); 52 | } 53 | 54 | public function isInternal(): TrinaryLogic 55 | { 56 | return TrinaryLogic::createNo(); 57 | } 58 | 59 | public function isStatic(): bool 60 | { 61 | return $this->isStatic; 62 | } 63 | 64 | public function getDocComment(): ?string 65 | { 66 | if (!$this->docComment || is_bool($this->docComment)) { 67 | return null; 68 | } 69 | 70 | return $this->docComment; 71 | } 72 | 73 | public function getName(): string 74 | { 75 | return $this->methodName; 76 | } 77 | 78 | public function isDeprecated(): TrinaryLogic 79 | { 80 | return TrinaryLogic::createNo(); 81 | } 82 | 83 | public function getPrototype(): ClassMemberReflection 84 | { 85 | return $this; 86 | } 87 | 88 | public function getVariants(): array 89 | { 90 | return [ 91 | new FunctionVariant( 92 | TemplateTypeMap::createEmpty(), 93 | null, 94 | $this->closureType->getParameters(), 95 | $this->closureType->isVariadic(), 96 | $this->closureType->getReturnType() 97 | ), 98 | ]; 99 | } 100 | 101 | public function getDeprecatedDescription(): ?string 102 | { 103 | return null; 104 | } 105 | 106 | public function getThrowType(): ?Type 107 | { 108 | return null; 109 | } 110 | 111 | public function hasSideEffects(): TrinaryLogic 112 | { 113 | return TrinaryLogic::createMaybe(); 114 | } 115 | } 116 | --------------------------------------------------------------------------------