├── vendor ├── composer │ ├── autoload_namespaces.php │ ├── autoload_psr4.php │ ├── autoload_classmap.php │ ├── installed.php │ ├── autoload_real.php │ ├── autoload_static.php │ ├── InstalledVersions.php │ └── ClassLoader.php └── autoload.php ├── LICENSE ├── composer.json ├── classes ├── FieldComposer.php └── FieldMethods.php ├── index.php ├── CHANGELOG.md └── README.md /vendor/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | array($baseDir . '/classes'), 10 | 'Kirby\\' => array($vendorDir . '/getkirby/composer-installer/src'), 11 | ); 12 | -------------------------------------------------------------------------------- /vendor/autoload.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | 'Kirby\\ComposerInstaller\\CmsInstaller' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php', 11 | 'Kirby\\ComposerInstaller\\Installer' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/Installer.php', 12 | 'Kirby\\ComposerInstaller\\Plugin' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/Plugin.php', 13 | 'Kirby\\ComposerInstaller\\PluginInstaller' => $vendorDir . '/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php', 14 | 'trych\\FieldComposer\\FieldComposer' => $baseDir . '/classes/FieldComposer.php', 15 | 'trych\\FieldComposer\\FieldMethods' => $baseDir . '/classes/FieldMethods.php', 16 | ); 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Timo Rychert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /vendor/composer/installed.php: -------------------------------------------------------------------------------- 1 | array( 3 | 'name' => 'trych/kirby-field-composer', 4 | 'pretty_version' => '2.0.2', 5 | 'version' => '2.0.2.0', 6 | 'reference' => null, 7 | 'type' => 'kirby-plugin', 8 | 'install_path' => __DIR__ . '/../../', 9 | 'aliases' => array(), 10 | 'dev' => true, 11 | ), 12 | 'versions' => array( 13 | 'getkirby/composer-installer' => array( 14 | 'pretty_version' => '1.2.1', 15 | 'version' => '1.2.1.0', 16 | 'reference' => 'c98ece30bfba45be7ce457e1102d1b169d922f3d', 17 | 'type' => 'composer-plugin', 18 | 'install_path' => __DIR__ . '/../getkirby/composer-installer', 19 | 'aliases' => array(), 20 | 'dev_requirement' => false, 21 | ), 22 | 'trych/kirby-field-composer' => array( 23 | 'pretty_version' => '2.0.2', 24 | 'version' => '2.0.2.0', 25 | 'reference' => null, 26 | 'type' => 'kirby-plugin', 27 | 'install_path' => __DIR__ . '/../../', 28 | 'aliases' => array(), 29 | 'dev_requirement' => false, 30 | ), 31 | ), 32 | ); 33 | -------------------------------------------------------------------------------- /vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | register(true); 33 | 34 | return $loader; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trych/kirby-field-composer", 3 | "description": "Kirby plugin providing methods to intelligently handle field values to compose complex strings.", 4 | "version": "2.0.2", 5 | "license": "MIT", 6 | "type": "kirby-plugin", 7 | "homepage": "https://github.com/trych/kirby-field-composer", 8 | "authors": [ 9 | { 10 | "name": "Timo Rychert", 11 | "email": "mail@trych.dev", 12 | "homepage": "https://trych.dev" 13 | } 14 | ], 15 | "support": { 16 | "issues": "https://github.com/trych/kirby-field-composer/issues", 17 | "source": "https://github.com/trych/kirby-field-composer", 18 | "docs": "https://github.com/trych/kirby-field-composer" 19 | }, 20 | "require": { 21 | "getkirby/composer-installer": "^1.1" 22 | }, 23 | "extra": { 24 | "installer-name": "field-composer" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "trych\\FieldComposer\\": "classes/" 29 | } 30 | }, 31 | "config": { 32 | "optimize-autoloader": true, 33 | "allow-plugins": { 34 | "getkirby/composer-installer": true 35 | } 36 | }, 37 | "keywords": [ 38 | "php", 39 | "plugin", 40 | "kirby", 41 | "kirby-cms", 42 | "kirby-plugin", 43 | "field", 44 | "field-methods", 45 | "method-chaining", 46 | "templating", 47 | "strings", 48 | "content-fields", 49 | "helper-functions" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /classes/FieldComposer.php: -------------------------------------------------------------------------------- 1 | 1) { 33 | if (is_string(end($fields))) { 34 | $separator = array_pop($fields); 35 | } 36 | } 37 | 38 | $fieldValues = array_values(array_filter( 39 | array_map( 40 | function($arg) { 41 | if (is_array($arg)) { 42 | // Recursive call for arrays 43 | return self::compose(...$arg); 44 | } 45 | return $arg instanceof Field ? $arg->value() : $arg; 46 | }, 47 | $fields 48 | ), 49 | fn($value) => $value !== null && $value !== '' && $value !== [] 50 | )); 51 | 52 | return new Field(null, '', implode($separator, $fieldValues)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | [ 13 | 'merge' => [FieldMethods::class, 'merge'], 14 | 'format' => [FieldMethods::class, 'format'], 15 | 'prefix' => [FieldMethods::class, 'prefix'], 16 | 'suffix' => [FieldMethods::class, 'suffix'], 17 | 'when' => [FieldMethods::class, 'when'], 18 | 'whenAny' => [FieldMethods::class, 'whenAny'], 19 | 'notWhen' => [FieldMethods::class, 'notWhen'], 20 | 'notWhenAny' => [FieldMethods::class, 'notWhenAny'], 21 | 'whenAll' => [FieldMethods::class, 'whenAll'], 22 | 'whenNone' => [FieldMethods::class, 'whenNone'], 23 | 'match' => [FieldMethods::class, 'match'], 24 | 'wrap' => [FieldMethods::class, 'wrap'], 25 | 'tag' => [FieldMethods::class, 'tag'], 26 | 'list' => [FieldMethods::class, 'list'], 27 | 'count' => [FieldMethods::class, 'count'], 28 | 'str' => [FieldMethods::class, 'str'], 29 | 'dump' => [FieldMethods::class, 'dump'], 30 | 'log' => [FieldMethods::class, 'log'], 31 | ], 32 | 33 | 'options' => [ 34 | 'mergeSeparator' => ', ', 35 | 'affixSeparator' => '', 36 | 'listJoinSeparator' => ', ', 37 | 'listConjunction' => null 38 | ], 39 | 40 | 'hooks' => [ 41 | 'system.loadPlugins:after' => function () { 42 | // register field helper functions 43 | 44 | if (Helpers::hasOverride('field') === false && !function_exists('field')) { 45 | function field(...$args): Field { 46 | return FieldComposer::compose(...$args); 47 | } 48 | } 49 | 50 | if (Helpers::hasOverride('f') === false && !function_exists('f')) { 51 | function f(...$args): Field { 52 | return FieldComposer::compose(...$args); 53 | } 54 | } 55 | } 56 | ] 57 | ] 58 | ); 59 | -------------------------------------------------------------------------------- /vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'trych\\FieldComposer\\' => 20, 13 | ), 14 | 'K' => 15 | array ( 16 | 'Kirby\\' => 6, 17 | ), 18 | ); 19 | 20 | public static $prefixDirsPsr4 = array ( 21 | 'trych\\FieldComposer\\' => 22 | array ( 23 | 0 => __DIR__ . '/../..' . '/classes', 24 | ), 25 | 'Kirby\\' => 26 | array ( 27 | 0 => __DIR__ . '/..' . '/getkirby/composer-installer/src', 28 | ), 29 | ); 30 | 31 | public static $classMap = array ( 32 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 33 | 'Kirby\\ComposerInstaller\\CmsInstaller' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/CmsInstaller.php', 34 | 'Kirby\\ComposerInstaller\\Installer' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/Installer.php', 35 | 'Kirby\\ComposerInstaller\\Plugin' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/Plugin.php', 36 | 'Kirby\\ComposerInstaller\\PluginInstaller' => __DIR__ . '/..' . '/getkirby/composer-installer/src/ComposerInstaller/PluginInstaller.php', 37 | 'trych\\FieldComposer\\FieldComposer' => __DIR__ . '/../..' . '/classes/FieldComposer.php', 38 | 'trych\\FieldComposer\\FieldMethods' => __DIR__ . '/../..' . '/classes/FieldMethods.php', 39 | ); 40 | 41 | public static function getInitializer(ClassLoader $loader) 42 | { 43 | return \Closure::bind(function () use ($loader) { 44 | $loader->prefixLengthsPsr4 = ComposerStaticInit3253e95ee6771c970b26c90d5e179b09::$prefixLengthsPsr4; 45 | $loader->prefixDirsPsr4 = ComposerStaticInit3253e95ee6771c970b26c90d5e179b09::$prefixDirsPsr4; 46 | $loader->classMap = ComposerStaticInit3253e95ee6771c970b26c90d5e179b09::$classMap; 47 | 48 | }, null, ClassLoader::class); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [2.0.0] - 2025-01-05 6 | 7 | ### Added 8 | - New `match()` method to compare field values against conditions with fallback options 9 | - New `list()` method to format fields as lists with powerful processing options 10 | - New `count()` method to count items in fields with the same options as `list()` 11 | - New `encode` parameter for `tag()` method to support nesting of HTML tags 12 | 13 | ### Changed 14 | - Added optional `$when` parameter to `format()`, `dump()` and `log()` methods for conditional execution 15 | 16 | ### Breaking Changes 17 | - Reordered parameters in `tag()` method: The newly introduced `$encode` parameter now precedes the `$when` parameter to maintain consistency with other methods. If you were calling the `tag()` method with positional arguments for the `$when` parameter, you will need to update your code to either use named arguments or add an explicit extra `$encode` parameter before your `$when` parameter. 18 | 19 | ## [1.5.0] - 2024-12-23 20 | 21 | ### Changed 22 | - Improved argument handling: `merge()`, `field()` and `f()` never treat single string arguments 23 | as separator 24 | 25 | ## [1.4.0] - 2024-11-10 26 | 27 | ### Added 28 | - New `dump()` method for debugging field values and objects with customizable message templates 29 | - New `log()` method for logging field values to files with timestamp and message support 30 | - CHANGELOG.md file 31 | 32 | ## [1.3.0] - 2024-09-16 33 | 34 | ### Changed 35 | - Improved argument handling: last string argument is now consistently treated as separator 36 | - Enhanced nested array support in field composition 37 | 38 | ## [1.2.0] - 2024-09-05 39 | 40 | ### Changed 41 | - Renamed parameters for better named argument support 42 | - Simplified complex usage examples in documentation 43 | 44 | ### Removed 45 | - Legacy separator option 46 | 47 | ## [1.1.0] - 2024-09-05 48 | 49 | ### Added 50 | - Conditional parameter support for `prefix()`, `suffix()`, `wrap()` and `tag()` methods 51 | 52 | ### Changed 53 | - Split `defaultSeparator` option into `mergeSeparator` and `affixSeparator` 54 | 55 | ### Fixed 56 | - Field helper output with single string argument 57 | 58 | ## [1.0.0] - 2024-09-03 59 | 60 | ### Added 61 | - Initial release 62 | -------------------------------------------------------------------------------- /vendor/composer/InstalledVersions.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer; 14 | 15 | use Composer\Autoload\ClassLoader; 16 | use Composer\Semver\VersionParser; 17 | 18 | /** 19 | * This class is copied in every Composer installed project and available to all 20 | * 21 | * See also https://getcomposer.org/doc/07-runtime.md#installed-versions 22 | * 23 | * To require its presence, you can require `composer-runtime-api ^2.0` 24 | * 25 | * @final 26 | */ 27 | class InstalledVersions 28 | { 29 | /** 30 | * @var mixed[]|null 31 | * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null 32 | */ 33 | private static $installed; 34 | 35 | /** 36 | * @var bool|null 37 | */ 38 | private static $canGetVendors; 39 | 40 | /** 41 | * @var array[] 42 | * @psalm-var array}> 43 | */ 44 | private static $installedByVendor = array(); 45 | 46 | /** 47 | * Returns a list of all package names which are present, either by being installed, replaced or provided 48 | * 49 | * @return string[] 50 | * @psalm-return list 51 | */ 52 | public static function getInstalledPackages() 53 | { 54 | $packages = array(); 55 | foreach (self::getInstalled() as $installed) { 56 | $packages[] = array_keys($installed['versions']); 57 | } 58 | 59 | if (1 === \count($packages)) { 60 | return $packages[0]; 61 | } 62 | 63 | return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); 64 | } 65 | 66 | /** 67 | * Returns a list of all package names with a specific type e.g. 'library' 68 | * 69 | * @param string $type 70 | * @return string[] 71 | * @psalm-return list 72 | */ 73 | public static function getInstalledPackagesByType($type) 74 | { 75 | $packagesByType = array(); 76 | 77 | foreach (self::getInstalled() as $installed) { 78 | foreach ($installed['versions'] as $name => $package) { 79 | if (isset($package['type']) && $package['type'] === $type) { 80 | $packagesByType[] = $name; 81 | } 82 | } 83 | } 84 | 85 | return $packagesByType; 86 | } 87 | 88 | /** 89 | * Checks whether the given package is installed 90 | * 91 | * This also returns true if the package name is provided or replaced by another package 92 | * 93 | * @param string $packageName 94 | * @param bool $includeDevRequirements 95 | * @return bool 96 | */ 97 | public static function isInstalled($packageName, $includeDevRequirements = true) 98 | { 99 | foreach (self::getInstalled() as $installed) { 100 | if (isset($installed['versions'][$packageName])) { 101 | return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; 102 | } 103 | } 104 | 105 | return false; 106 | } 107 | 108 | /** 109 | * Checks whether the given package satisfies a version constraint 110 | * 111 | * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: 112 | * 113 | * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') 114 | * 115 | * @param VersionParser $parser Install composer/semver to have access to this class and functionality 116 | * @param string $packageName 117 | * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package 118 | * @return bool 119 | */ 120 | public static function satisfies(VersionParser $parser, $packageName, $constraint) 121 | { 122 | $constraint = $parser->parseConstraints((string) $constraint); 123 | $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); 124 | 125 | return $provided->matches($constraint); 126 | } 127 | 128 | /** 129 | * Returns a version constraint representing all the range(s) which are installed for a given package 130 | * 131 | * It is easier to use this via isInstalled() with the $constraint argument if you need to check 132 | * whether a given version of a package is installed, and not just whether it exists 133 | * 134 | * @param string $packageName 135 | * @return string Version constraint usable with composer/semver 136 | */ 137 | public static function getVersionRanges($packageName) 138 | { 139 | foreach (self::getInstalled() as $installed) { 140 | if (!isset($installed['versions'][$packageName])) { 141 | continue; 142 | } 143 | 144 | $ranges = array(); 145 | if (isset($installed['versions'][$packageName]['pretty_version'])) { 146 | $ranges[] = $installed['versions'][$packageName]['pretty_version']; 147 | } 148 | if (array_key_exists('aliases', $installed['versions'][$packageName])) { 149 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); 150 | } 151 | if (array_key_exists('replaced', $installed['versions'][$packageName])) { 152 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); 153 | } 154 | if (array_key_exists('provided', $installed['versions'][$packageName])) { 155 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); 156 | } 157 | 158 | return implode(' || ', $ranges); 159 | } 160 | 161 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 162 | } 163 | 164 | /** 165 | * @param string $packageName 166 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 167 | */ 168 | public static function getVersion($packageName) 169 | { 170 | foreach (self::getInstalled() as $installed) { 171 | if (!isset($installed['versions'][$packageName])) { 172 | continue; 173 | } 174 | 175 | if (!isset($installed['versions'][$packageName]['version'])) { 176 | return null; 177 | } 178 | 179 | return $installed['versions'][$packageName]['version']; 180 | } 181 | 182 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 183 | } 184 | 185 | /** 186 | * @param string $packageName 187 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 188 | */ 189 | public static function getPrettyVersion($packageName) 190 | { 191 | foreach (self::getInstalled() as $installed) { 192 | if (!isset($installed['versions'][$packageName])) { 193 | continue; 194 | } 195 | 196 | if (!isset($installed['versions'][$packageName]['pretty_version'])) { 197 | return null; 198 | } 199 | 200 | return $installed['versions'][$packageName]['pretty_version']; 201 | } 202 | 203 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 204 | } 205 | 206 | /** 207 | * @param string $packageName 208 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference 209 | */ 210 | public static function getReference($packageName) 211 | { 212 | foreach (self::getInstalled() as $installed) { 213 | if (!isset($installed['versions'][$packageName])) { 214 | continue; 215 | } 216 | 217 | if (!isset($installed['versions'][$packageName]['reference'])) { 218 | return null; 219 | } 220 | 221 | return $installed['versions'][$packageName]['reference']; 222 | } 223 | 224 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 225 | } 226 | 227 | /** 228 | * @param string $packageName 229 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. 230 | */ 231 | public static function getInstallPath($packageName) 232 | { 233 | foreach (self::getInstalled() as $installed) { 234 | if (!isset($installed['versions'][$packageName])) { 235 | continue; 236 | } 237 | 238 | return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; 239 | } 240 | 241 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 242 | } 243 | 244 | /** 245 | * @return array 246 | * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} 247 | */ 248 | public static function getRootPackage() 249 | { 250 | $installed = self::getInstalled(); 251 | 252 | return $installed[0]['root']; 253 | } 254 | 255 | /** 256 | * Returns the raw installed.php data for custom implementations 257 | * 258 | * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. 259 | * @return array[] 260 | * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} 261 | */ 262 | public static function getRawData() 263 | { 264 | @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); 265 | 266 | if (null === self::$installed) { 267 | // only require the installed.php file if this file is loaded from its dumped location, 268 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 269 | if (substr(__DIR__, -8, 1) !== 'C') { 270 | self::$installed = include __DIR__ . '/installed.php'; 271 | } else { 272 | self::$installed = array(); 273 | } 274 | } 275 | 276 | return self::$installed; 277 | } 278 | 279 | /** 280 | * Returns the raw data of all installed.php which are currently loaded for custom implementations 281 | * 282 | * @return array[] 283 | * @psalm-return list}> 284 | */ 285 | public static function getAllRawData() 286 | { 287 | return self::getInstalled(); 288 | } 289 | 290 | /** 291 | * Lets you reload the static array from another file 292 | * 293 | * This is only useful for complex integrations in which a project needs to use 294 | * this class but then also needs to execute another project's autoloader in process, 295 | * and wants to ensure both projects have access to their version of installed.php. 296 | * 297 | * A typical case would be PHPUnit, where it would need to make sure it reads all 298 | * the data it needs from this class, then call reload() with 299 | * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure 300 | * the project in which it runs can then also use this class safely, without 301 | * interference between PHPUnit's dependencies and the project's dependencies. 302 | * 303 | * @param array[] $data A vendor/composer/installed.php data set 304 | * @return void 305 | * 306 | * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data 307 | */ 308 | public static function reload($data) 309 | { 310 | self::$installed = $data; 311 | self::$installedByVendor = array(); 312 | } 313 | 314 | /** 315 | * @return array[] 316 | * @psalm-return list}> 317 | */ 318 | private static function getInstalled() 319 | { 320 | if (null === self::$canGetVendors) { 321 | self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); 322 | } 323 | 324 | $installed = array(); 325 | 326 | if (self::$canGetVendors) { 327 | foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { 328 | if (isset(self::$installedByVendor[$vendorDir])) { 329 | $installed[] = self::$installedByVendor[$vendorDir]; 330 | } elseif (is_file($vendorDir.'/composer/installed.php')) { 331 | /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ 332 | $required = require $vendorDir.'/composer/installed.php'; 333 | $installed[] = self::$installedByVendor[$vendorDir] = $required; 334 | if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { 335 | self::$installed = $installed[count($installed) - 1]; 336 | } 337 | } 338 | } 339 | } 340 | 341 | if (null === self::$installed) { 342 | // only require the installed.php file if this file is loaded from its dumped location, 343 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 344 | if (substr(__DIR__, -8, 1) !== 'C') { 345 | /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ 346 | $required = require __DIR__ . '/installed.php'; 347 | self::$installed = $required; 348 | } else { 349 | self::$installed = array(); 350 | } 351 | } 352 | 353 | if (self::$installed !== array()) { 354 | $installed[] = self::$installed; 355 | } 356 | 357 | return $installed; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /vendor/composer/ClassLoader.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer\Autoload; 14 | 15 | /** 16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 | * 18 | * $loader = new \Composer\Autoload\ClassLoader(); 19 | * 20 | * // register classes with namespaces 21 | * $loader->add('Symfony\Component', __DIR__.'/component'); 22 | * $loader->add('Symfony', __DIR__.'/framework'); 23 | * 24 | * // activate the autoloader 25 | * $loader->register(); 26 | * 27 | * // to enable searching the include path (eg. for PEAR packages) 28 | * $loader->setUseIncludePath(true); 29 | * 30 | * In this example, if you try to use a class in the Symfony\Component 31 | * namespace or one of its children (Symfony\Component\Console for instance), 32 | * the autoloader will first look for the class under the component/ 33 | * directory, and it will then fallback to the framework/ directory if not 34 | * found before giving up. 35 | * 36 | * This class is loosely based on the Symfony UniversalClassLoader. 37 | * 38 | * @author Fabien Potencier 39 | * @author Jordi Boggiano 40 | * @see https://www.php-fig.org/psr/psr-0/ 41 | * @see https://www.php-fig.org/psr/psr-4/ 42 | */ 43 | class ClassLoader 44 | { 45 | /** @var \Closure(string):void */ 46 | private static $includeFile; 47 | 48 | /** @var string|null */ 49 | private $vendorDir; 50 | 51 | // PSR-4 52 | /** 53 | * @var array> 54 | */ 55 | private $prefixLengthsPsr4 = array(); 56 | /** 57 | * @var array> 58 | */ 59 | private $prefixDirsPsr4 = array(); 60 | /** 61 | * @var list 62 | */ 63 | private $fallbackDirsPsr4 = array(); 64 | 65 | // PSR-0 66 | /** 67 | * List of PSR-0 prefixes 68 | * 69 | * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) 70 | * 71 | * @var array>> 72 | */ 73 | private $prefixesPsr0 = array(); 74 | /** 75 | * @var list 76 | */ 77 | private $fallbackDirsPsr0 = array(); 78 | 79 | /** @var bool */ 80 | private $useIncludePath = false; 81 | 82 | /** 83 | * @var array 84 | */ 85 | private $classMap = array(); 86 | 87 | /** @var bool */ 88 | private $classMapAuthoritative = false; 89 | 90 | /** 91 | * @var array 92 | */ 93 | private $missingClasses = array(); 94 | 95 | /** @var string|null */ 96 | private $apcuPrefix; 97 | 98 | /** 99 | * @var array 100 | */ 101 | private static $registeredLoaders = array(); 102 | 103 | /** 104 | * @param string|null $vendorDir 105 | */ 106 | public function __construct($vendorDir = null) 107 | { 108 | $this->vendorDir = $vendorDir; 109 | self::initializeIncludeClosure(); 110 | } 111 | 112 | /** 113 | * @return array> 114 | */ 115 | public function getPrefixes() 116 | { 117 | if (!empty($this->prefixesPsr0)) { 118 | return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); 119 | } 120 | 121 | return array(); 122 | } 123 | 124 | /** 125 | * @return array> 126 | */ 127 | public function getPrefixesPsr4() 128 | { 129 | return $this->prefixDirsPsr4; 130 | } 131 | 132 | /** 133 | * @return list 134 | */ 135 | public function getFallbackDirs() 136 | { 137 | return $this->fallbackDirsPsr0; 138 | } 139 | 140 | /** 141 | * @return list 142 | */ 143 | public function getFallbackDirsPsr4() 144 | { 145 | return $this->fallbackDirsPsr4; 146 | } 147 | 148 | /** 149 | * @return array Array of classname => path 150 | */ 151 | public function getClassMap() 152 | { 153 | return $this->classMap; 154 | } 155 | 156 | /** 157 | * @param array $classMap Class to filename map 158 | * 159 | * @return void 160 | */ 161 | public function addClassMap(array $classMap) 162 | { 163 | if ($this->classMap) { 164 | $this->classMap = array_merge($this->classMap, $classMap); 165 | } else { 166 | $this->classMap = $classMap; 167 | } 168 | } 169 | 170 | /** 171 | * Registers a set of PSR-0 directories for a given prefix, either 172 | * appending or prepending to the ones previously set for this prefix. 173 | * 174 | * @param string $prefix The prefix 175 | * @param list|string $paths The PSR-0 root directories 176 | * @param bool $prepend Whether to prepend the directories 177 | * 178 | * @return void 179 | */ 180 | public function add($prefix, $paths, $prepend = false) 181 | { 182 | $paths = (array) $paths; 183 | if (!$prefix) { 184 | if ($prepend) { 185 | $this->fallbackDirsPsr0 = array_merge( 186 | $paths, 187 | $this->fallbackDirsPsr0 188 | ); 189 | } else { 190 | $this->fallbackDirsPsr0 = array_merge( 191 | $this->fallbackDirsPsr0, 192 | $paths 193 | ); 194 | } 195 | 196 | return; 197 | } 198 | 199 | $first = $prefix[0]; 200 | if (!isset($this->prefixesPsr0[$first][$prefix])) { 201 | $this->prefixesPsr0[$first][$prefix] = $paths; 202 | 203 | return; 204 | } 205 | if ($prepend) { 206 | $this->prefixesPsr0[$first][$prefix] = array_merge( 207 | $paths, 208 | $this->prefixesPsr0[$first][$prefix] 209 | ); 210 | } else { 211 | $this->prefixesPsr0[$first][$prefix] = array_merge( 212 | $this->prefixesPsr0[$first][$prefix], 213 | $paths 214 | ); 215 | } 216 | } 217 | 218 | /** 219 | * Registers a set of PSR-4 directories for a given namespace, either 220 | * appending or prepending to the ones previously set for this namespace. 221 | * 222 | * @param string $prefix The prefix/namespace, with trailing '\\' 223 | * @param list|string $paths The PSR-4 base directories 224 | * @param bool $prepend Whether to prepend the directories 225 | * 226 | * @throws \InvalidArgumentException 227 | * 228 | * @return void 229 | */ 230 | public function addPsr4($prefix, $paths, $prepend = false) 231 | { 232 | $paths = (array) $paths; 233 | if (!$prefix) { 234 | // Register directories for the root namespace. 235 | if ($prepend) { 236 | $this->fallbackDirsPsr4 = array_merge( 237 | $paths, 238 | $this->fallbackDirsPsr4 239 | ); 240 | } else { 241 | $this->fallbackDirsPsr4 = array_merge( 242 | $this->fallbackDirsPsr4, 243 | $paths 244 | ); 245 | } 246 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 247 | // Register directories for a new namespace. 248 | $length = strlen($prefix); 249 | if ('\\' !== $prefix[$length - 1]) { 250 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 251 | } 252 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 253 | $this->prefixDirsPsr4[$prefix] = $paths; 254 | } elseif ($prepend) { 255 | // Prepend directories for an already registered namespace. 256 | $this->prefixDirsPsr4[$prefix] = array_merge( 257 | $paths, 258 | $this->prefixDirsPsr4[$prefix] 259 | ); 260 | } else { 261 | // Append directories for an already registered namespace. 262 | $this->prefixDirsPsr4[$prefix] = array_merge( 263 | $this->prefixDirsPsr4[$prefix], 264 | $paths 265 | ); 266 | } 267 | } 268 | 269 | /** 270 | * Registers a set of PSR-0 directories for a given prefix, 271 | * replacing any others previously set for this prefix. 272 | * 273 | * @param string $prefix The prefix 274 | * @param list|string $paths The PSR-0 base directories 275 | * 276 | * @return void 277 | */ 278 | public function set($prefix, $paths) 279 | { 280 | if (!$prefix) { 281 | $this->fallbackDirsPsr0 = (array) $paths; 282 | } else { 283 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 284 | } 285 | } 286 | 287 | /** 288 | * Registers a set of PSR-4 directories for a given namespace, 289 | * replacing any others previously set for this namespace. 290 | * 291 | * @param string $prefix The prefix/namespace, with trailing '\\' 292 | * @param list|string $paths The PSR-4 base directories 293 | * 294 | * @throws \InvalidArgumentException 295 | * 296 | * @return void 297 | */ 298 | public function setPsr4($prefix, $paths) 299 | { 300 | if (!$prefix) { 301 | $this->fallbackDirsPsr4 = (array) $paths; 302 | } else { 303 | $length = strlen($prefix); 304 | if ('\\' !== $prefix[$length - 1]) { 305 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 306 | } 307 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 308 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 309 | } 310 | } 311 | 312 | /** 313 | * Turns on searching the include path for class files. 314 | * 315 | * @param bool $useIncludePath 316 | * 317 | * @return void 318 | */ 319 | public function setUseIncludePath($useIncludePath) 320 | { 321 | $this->useIncludePath = $useIncludePath; 322 | } 323 | 324 | /** 325 | * Can be used to check if the autoloader uses the include path to check 326 | * for classes. 327 | * 328 | * @return bool 329 | */ 330 | public function getUseIncludePath() 331 | { 332 | return $this->useIncludePath; 333 | } 334 | 335 | /** 336 | * Turns off searching the prefix and fallback directories for classes 337 | * that have not been registered with the class map. 338 | * 339 | * @param bool $classMapAuthoritative 340 | * 341 | * @return void 342 | */ 343 | public function setClassMapAuthoritative($classMapAuthoritative) 344 | { 345 | $this->classMapAuthoritative = $classMapAuthoritative; 346 | } 347 | 348 | /** 349 | * Should class lookup fail if not found in the current class map? 350 | * 351 | * @return bool 352 | */ 353 | public function isClassMapAuthoritative() 354 | { 355 | return $this->classMapAuthoritative; 356 | } 357 | 358 | /** 359 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 360 | * 361 | * @param string|null $apcuPrefix 362 | * 363 | * @return void 364 | */ 365 | public function setApcuPrefix($apcuPrefix) 366 | { 367 | $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; 368 | } 369 | 370 | /** 371 | * The APCu prefix in use, or null if APCu caching is not enabled. 372 | * 373 | * @return string|null 374 | */ 375 | public function getApcuPrefix() 376 | { 377 | return $this->apcuPrefix; 378 | } 379 | 380 | /** 381 | * Registers this instance as an autoloader. 382 | * 383 | * @param bool $prepend Whether to prepend the autoloader or not 384 | * 385 | * @return void 386 | */ 387 | public function register($prepend = false) 388 | { 389 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 390 | 391 | if (null === $this->vendorDir) { 392 | return; 393 | } 394 | 395 | if ($prepend) { 396 | self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; 397 | } else { 398 | unset(self::$registeredLoaders[$this->vendorDir]); 399 | self::$registeredLoaders[$this->vendorDir] = $this; 400 | } 401 | } 402 | 403 | /** 404 | * Unregisters this instance as an autoloader. 405 | * 406 | * @return void 407 | */ 408 | public function unregister() 409 | { 410 | spl_autoload_unregister(array($this, 'loadClass')); 411 | 412 | if (null !== $this->vendorDir) { 413 | unset(self::$registeredLoaders[$this->vendorDir]); 414 | } 415 | } 416 | 417 | /** 418 | * Loads the given class or interface. 419 | * 420 | * @param string $class The name of the class 421 | * @return true|null True if loaded, null otherwise 422 | */ 423 | public function loadClass($class) 424 | { 425 | if ($file = $this->findFile($class)) { 426 | $includeFile = self::$includeFile; 427 | $includeFile($file); 428 | 429 | return true; 430 | } 431 | 432 | return null; 433 | } 434 | 435 | /** 436 | * Finds the path to the file where the class is defined. 437 | * 438 | * @param string $class The name of the class 439 | * 440 | * @return string|false The path if found, false otherwise 441 | */ 442 | public function findFile($class) 443 | { 444 | // class map lookup 445 | if (isset($this->classMap[$class])) { 446 | return $this->classMap[$class]; 447 | } 448 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 449 | return false; 450 | } 451 | if (null !== $this->apcuPrefix) { 452 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 453 | if ($hit) { 454 | return $file; 455 | } 456 | } 457 | 458 | $file = $this->findFileWithExtension($class, '.php'); 459 | 460 | // Search for Hack files if we are running on HHVM 461 | if (false === $file && defined('HHVM_VERSION')) { 462 | $file = $this->findFileWithExtension($class, '.hh'); 463 | } 464 | 465 | if (null !== $this->apcuPrefix) { 466 | apcu_add($this->apcuPrefix.$class, $file); 467 | } 468 | 469 | if (false === $file) { 470 | // Remember that this class does not exist. 471 | $this->missingClasses[$class] = true; 472 | } 473 | 474 | return $file; 475 | } 476 | 477 | /** 478 | * Returns the currently registered loaders keyed by their corresponding vendor directories. 479 | * 480 | * @return array 481 | */ 482 | public static function getRegisteredLoaders() 483 | { 484 | return self::$registeredLoaders; 485 | } 486 | 487 | /** 488 | * @param string $class 489 | * @param string $ext 490 | * @return string|false 491 | */ 492 | private function findFileWithExtension($class, $ext) 493 | { 494 | // PSR-4 lookup 495 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 496 | 497 | $first = $class[0]; 498 | if (isset($this->prefixLengthsPsr4[$first])) { 499 | $subPath = $class; 500 | while (false !== $lastPos = strrpos($subPath, '\\')) { 501 | $subPath = substr($subPath, 0, $lastPos); 502 | $search = $subPath . '\\'; 503 | if (isset($this->prefixDirsPsr4[$search])) { 504 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 505 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 506 | if (file_exists($file = $dir . $pathEnd)) { 507 | return $file; 508 | } 509 | } 510 | } 511 | } 512 | } 513 | 514 | // PSR-4 fallback dirs 515 | foreach ($this->fallbackDirsPsr4 as $dir) { 516 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 517 | return $file; 518 | } 519 | } 520 | 521 | // PSR-0 lookup 522 | if (false !== $pos = strrpos($class, '\\')) { 523 | // namespaced class name 524 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 525 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 526 | } else { 527 | // PEAR-like class name 528 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 529 | } 530 | 531 | if (isset($this->prefixesPsr0[$first])) { 532 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 533 | if (0 === strpos($class, $prefix)) { 534 | foreach ($dirs as $dir) { 535 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 536 | return $file; 537 | } 538 | } 539 | } 540 | } 541 | } 542 | 543 | // PSR-0 fallback dirs 544 | foreach ($this->fallbackDirsPsr0 as $dir) { 545 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 546 | return $file; 547 | } 548 | } 549 | 550 | // PSR-0 include paths. 551 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 552 | return $file; 553 | } 554 | 555 | return false; 556 | } 557 | 558 | /** 559 | * @return void 560 | */ 561 | private static function initializeIncludeClosure() 562 | { 563 | if (self::$includeFile !== null) { 564 | return; 565 | } 566 | 567 | /** 568 | * Scope isolated include. 569 | * 570 | * Prevents access to $this/self from included files. 571 | * 572 | * @param string $file 573 | * @return void 574 | */ 575 | self::$includeFile = \Closure::bind(static function($file) { 576 | include $file; 577 | }, null, null); 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /classes/FieldMethods.php: -------------------------------------------------------------------------------- 1 | value() : $arg; 72 | }, 73 | $fields 74 | ), 75 | fn($value) => $value !== null && $value !== '' && $value !== [] 76 | )); 77 | 78 | if ($includeCurrentField && $field->isNotEmpty()) { 79 | // Calculate the insertion index 80 | $insertIndex = $position >= 0 ? min($position, count($fieldValues)) : max(0, count($fieldValues) + $position + 1); 81 | 82 | // Insert the current field's value at the calculated position 83 | array_splice($fieldValues, $insertIndex, 0, [$field->value()]); 84 | } 85 | 86 | return $field->value(implode($separator, $fieldValues)); 87 | } 88 | 89 | /** 90 | * Applies a custom formatting function to the field's value. 91 | * 92 | * This method allows for flexible, user-defined transformations of the field's value 93 | * using a closure. The closure receives the field's current value and the field object itself, 94 | * allowing for complex transformations based on both the value and other field properties. 95 | * 96 | * @param Closure $callback A closure that takes the field's value and the field object as arguments, 97 | * and returns the new formatted value 98 | * @param mixed $when A condition that determines whether to format the field; defaults to true 99 | * 100 | * @return Field The field with its value transformed by the callback 101 | */ 102 | public static function format(Field $field, Closure $callback, mixed $when = true): Field 103 | { 104 | if (!self::isValidCondition($when)) return $field; 105 | 106 | return $field->value($callback($field->value(), $field)); 107 | } 108 | 109 | /** 110 | * Adds a prefix to the field's value. If the field is empty, no prefix is added. 111 | * 112 | * @param Field|string $prefix The prefix to add 113 | * @param string|null $separator An optional separator between the prefix and the field value 114 | * @param mixed $when A condition that determines whether to add the prefix; defaults to true 115 | * 116 | * @return Field The modified field 117 | */ 118 | public static function prefix(Field $field, Field|string $prefix = '', ?string $separator = null, mixed $when = true): Field 119 | { 120 | if ($field->isEmpty() || !self::isValidCondition($when)) return $field; 121 | 122 | $separator = $separator ?? option('trych.field-composer.affixSeparator'); 123 | return self::addAffix($field, $prefix, $separator, true); 124 | } 125 | 126 | /** 127 | * Adds a suffix to the field's value. If the field is empty, no suffix is added. 128 | * 129 | * @param Field|string $suffix The suffix to add 130 | * @param string|null $separator An optional separator between the field value and the suffix 131 | * @param mixed $when A condition that determines whether to add the suffix; defaults to true 132 | * 133 | * @return Field The modified field 134 | */ 135 | public static function suffix(Field $field, Field|string $suffix = '', ?string $separator = null, mixed $when = true): Field 136 | { 137 | if ($field->isEmpty() || !self::isValidCondition($when)) return $field; 138 | 139 | $separator = $separator ?? option('trych.field-composer.affixSeparator'); 140 | return self::addAffix($field, $suffix, $separator, false); 141 | } 142 | 143 | /** 144 | * Returns the field if all conditions are valid, otherwise returns an empty field. 145 | * 146 | * @param mixed ...$conditions Variable number of conditions to check 147 | * 148 | * @return Field The original field if all conditions are valid, otherwise an empty field 149 | */ 150 | public static function when(Field $field, mixed ...$conditions): Field 151 | { 152 | foreach ($conditions as $condition) { 153 | if (!self::isValidCondition($condition)) { 154 | return $field->value(''); 155 | } 156 | } 157 | return $field; 158 | } 159 | 160 | /** 161 | * Returns the field if any condition is valid, otherwise returns an empty field. 162 | * 163 | * @param mixed ...$conditions Variable number of conditions to check 164 | * 165 | * @return Field The original field if any condition is valid, otherwise an empty field 166 | */ 167 | public static function whenAny(Field $field, mixed ...$conditions): Field 168 | { 169 | foreach ($conditions as $condition) { 170 | if (self::isValidCondition($condition)) { 171 | return $field; 172 | } 173 | } 174 | return $field->value(''); 175 | } 176 | 177 | /** 178 | * Returns an empty field if all conditions are valid, otherwise returns the original field. 179 | * 180 | * @param mixed ...$conditions Variable number of conditions to check 181 | * 182 | * @return Field An empty field if all conditions are valid, otherwise the original field 183 | */ 184 | public static function notWhen(Field $field, mixed ...$conditions): Field 185 | { 186 | foreach ($conditions as $condition) { 187 | if (!self::isValidCondition($condition)) { 188 | return $field; 189 | } 190 | } 191 | return $field->value(''); 192 | } 193 | 194 | /** 195 | * Returns an empty field if any of the conditions are valid, otherwise returns the original field. 196 | * 197 | * @param mixed ...$conditions Variable number of conditions to check 198 | * 199 | * @return Field An empty field if any of the conditions is valid, otherwise the original field 200 | */ 201 | public static function notWhenAny(Field $field, mixed ...$conditions): Field 202 | { 203 | foreach ($conditions as $condition) { 204 | if (self::isValidCondition($condition)) { 205 | return $field->value(''); 206 | } 207 | } 208 | return $field; 209 | } 210 | 211 | /** 212 | * Alias for when(). Returns the field if all conditions are valid. 213 | * 214 | * @see self::when() 215 | * 216 | * @param mixed ...$conditions Variable number of conditions to check 217 | * 218 | * @return Field The original field if all conditions are valid, otherwise an empty field 219 | */ 220 | public static function whenAll(Field $field, mixed ...$conditions): Field 221 | { 222 | return self::when($field, ...$conditions); 223 | } 224 | 225 | /** 226 | * Alias for notWhenAny(). Returns the field if all conditions are invalid. 227 | * 228 | * @see self::notWhenAny() 229 | * 230 | * @param mixed ...$conditions Variable number of conditions to check 231 | * 232 | * @return Field The original field if all conditions are invalid, otherwise an empty field 233 | */ 234 | public static function whenNone(Field $field, mixed ...$conditions): Field 235 | { 236 | return self::notWhenAny($field, ...$conditions); 237 | } 238 | 239 | /** 240 | * Matches the field's value against the keys of an array of key/value pairs and returns 241 | * the corresponding value of the first match. 242 | * If no values are matched, the original field will be returned. 243 | * If 'default' is set as the last key in the array, its value serves as a fallback if no 244 | * matches are found. 245 | * 246 | * @param array $conditions Array of key/value pairs where the keys are matched against the original 247 | * field's value and the keys value would be returned on a match. 248 | * Keys can be strings or integers but will be compared as strings. 249 | * Values can be strings or Field objects. 250 | * If 'default' is the last key, it acts as fallback for unmatched values. 251 | * @param mixed $when An optional condition that determines whether to use the match function the 252 | * field; defaults to `true`. If the condition is not met, the original field value 253 | * is returned unchanged. 254 | * 255 | * @return Field Returns a new field with either: 256 | * - the matched condition's value 257 | * - the default value (if provided as last key and no match found) 258 | * - the original field value (if no match and no default) 259 | */ 260 | public static function match(Field $field, array $conditions, mixed $when = true): Field 261 | { 262 | if (!self::isValidCondition($when)) { 263 | return $field; 264 | } 265 | 266 | $value = $field->value(); 267 | $keys = array_keys($conditions); 268 | $lastKey = end($keys); 269 | 270 | foreach ($conditions as $condition => $result) { 271 | // Only treat 'default' specially if it's the last key 272 | if ($condition === $lastKey && $condition === 'default') { 273 | continue; 274 | } 275 | 276 | if ((string)$condition === $value) { 277 | return $field->value( 278 | $result instanceof Field ? $result->value() : $result 279 | ); 280 | } 281 | } 282 | 283 | // If no match was found and last key is 'default', use that 284 | if ($lastKey === 'default') { 285 | return $field->value( 286 | $conditions['default'] instanceof Field ? $conditions['default']->value() : $conditions['default'] 287 | ); 288 | } 289 | 290 | return $field; 291 | } 292 | 293 | /** 294 | * Wraps the field's value with specified strings or field values. If the field is empty, no wrapping strings will be added. 295 | * 296 | * @param Field|string $before The string or field to prepend to the field's value 297 | * @param Field|string|null $after The string or field to append to the field's value. If null, $before is used 298 | * @param string|null $separator An optional separator between the field value and $before and $after 299 | * @param mixed $when A condition that determines whether to wrap the field; defaults to `true` 300 | * 301 | * @return Field The modified field with wrapped value, or the original field if it's empty 302 | */ 303 | public static function wrap( 304 | Field $field, 305 | Field|string $before, 306 | Field|string|null $after = null, 307 | ?string $separator = null, 308 | mixed $when = true 309 | ): Field 310 | { 311 | if ($field->isEmpty() || !self::isValidCondition($when)) return $field; 312 | 313 | $separator = $separator ?? option('trych.field-composer.affixSeparator'); 314 | $after = $after ?? $before; 315 | 316 | $prefixedField = self::addAffix($field, $before, $separator, true); 317 | return self::addAffix($prefixedField, $after, $separator, false); 318 | } 319 | 320 | /** 321 | * Wraps the field's value in an HTML tag. If the field is empty, no tags are added. 322 | * 323 | * @param string $tag The HTML tag to wrap the field's value in 324 | * @param array $attr An associative array of HTML attributes for the tag 325 | * @param string|null $indent The indentation string, or null for no indentation 326 | * @param int $level The indentation level 327 | * @param bool $encode If `true` (default), encodes HTML characters in content. Set to `false` for 328 | * outer tags in nested `tag()` calls to preserve inner HTML structure. 329 | * @param mixed $when A condition that determines whether to wrap the field in a tag; defaults to `true` 330 | * 331 | * @return Field The modified field with its value wrapped in the specified HTML tag 332 | */ 333 | public static function tag( 334 | Field $field, 335 | string $tag, 336 | array $attr = [], 337 | ?string $indent = null, 338 | int $level = 0, 339 | bool $encode = true, 340 | mixed $when = true 341 | ): Field 342 | { 343 | 344 | if ($field->isEmpty() || !self::isValidCondition($when)) return $field; 345 | 346 | $content = $encode ? $field->value() : [$field->value()]; 347 | 348 | return $field->value( 349 | Html::tag($tag, $content, $attr, $indent, $level) 350 | ); 351 | } 352 | 353 | /** 354 | * Converts a field's value into a formatted list. Works with any field type that can be 355 | * interpreted as a list: structure fields, pages fields, files fields, users fields, 356 | * blocks fields, or strings with a user defined separator. 357 | * 358 | * @param string|bool|null $split Pattern to split string value, `null` for auto-detect, 359 | * `false` to force array handling (non-array fields will be treated as single item) 360 | * @param string|null $join String to join list items; defaults to configured listJoinSeparator 361 | * @param string|null|callable $conjunction Optional conjunction text or callback before last item; 362 | * defaults to configured listConjunction 363 | * @param bool $serial Whether to use serial (Oxford) comma before conjunction; defaults to `false` 364 | * @param Closure|null $each Optional callback to process each item before joining 365 | * @param Closure|null $all Optional callback to process the entire list array right before formatting it to the list 366 | * @param mixed $when A condition that determines whether to process the field; defaults to `true` 367 | * 368 | * @return Field The processed field 369 | */ 370 | public static function list( 371 | Field $field, 372 | string|bool|null $split = null, 373 | ?string $join = null, 374 | ?string $conjunction = null, 375 | bool $serial = false, 376 | ?Closure $each = null, 377 | ?Closure $all = null, 378 | mixed $when = true 379 | ): Field { 380 | if ($field->isEmpty() || !self::isValidCondition($when)) { 381 | return $field; 382 | } 383 | 384 | $join = $join ?? option('trych.field-composer.listJoinSeparator'); 385 | $conjunction = $conjunction ?? option('trych.field-composer.listConjunction'); 386 | 387 | $items = self::getFieldItems($field, $split, $each); 388 | 389 | // Apply list callback if provided 390 | if ($all) { 391 | $items = $all($items); 392 | } 393 | 394 | $count = count($items); 395 | if ($count <= 1) { 396 | return $field->value($count ? $items[0] : ''); 397 | } 398 | 399 | if (!$conjunction) { 400 | return $field->value(implode($join, $items)); 401 | } 402 | 403 | if(is_callable($conjunction)) { 404 | $conjunction = $conjunction(); 405 | } 406 | 407 | $conjunction = ' ' . trim($conjunction) . ' '; 408 | 409 | if ($count === 2) { 410 | return $field->value($items[0] . $conjunction . $items[1]); 411 | } 412 | 413 | $last = array_pop($items); 414 | return $field->value( 415 | implode($join, $items) . 416 | ($serial ? $join : '') . 417 | $conjunction . 418 | $last 419 | ); 420 | } 421 | 422 | /** 423 | * Counts the number of items in a field that represents a list. 424 | * Works with any field type that can be interpreted as a list: structure fields, 425 | * pages fields, files fields, users fields, blocks fields, or strings with a user 426 | * defined separator. 427 | * 428 | * @param string|bool|null $split Pattern to split string value, `null` for auto-detect, 429 | * `false` to force array handling (non-array fields will be treated as single item) 430 | * @param Closure|null $each Optional callback to process each item before counting 431 | * @param Closure|null $all Optional callback to process the entire list array 432 | * @param mixed $when A condition that determines whether to process the field; defaults to `true` 433 | * 434 | * @return Field The processed field containing the count (`0` for empty fields) 435 | */ 436 | public static function count( 437 | Field $field, 438 | string|bool|null $split = null, 439 | ?Closure $each = null, 440 | ?Closure $all = null, 441 | mixed $when = true 442 | ): Field { 443 | if ($field->isEmpty() || !self::isValidCondition($when)) { 444 | return $field->value(0); 445 | } 446 | 447 | $items = self::getFieldItems($field, $split, $each); 448 | 449 | if ($all) { 450 | $items = $all($items); 451 | } 452 | 453 | return $field->value(count($items)); 454 | } 455 | 456 | /** 457 | * Applies a Kirby Str class method to the field's value. 458 | * 459 | * This method allows you to use any of Kirby's Str:: utility methods directly on a field. 460 | * It provides a convenient way to perform string operations that are not covered by existing field methods. 461 | * 462 | * @param string $method The name of the Str class method to apply 463 | * @param mixed ...$args Additional arguments to pass to the Str method 464 | * 465 | * @return Field The modified field with the Str method applied to its value 466 | * 467 | * @throws InvalidArgumentException If the specified method does not exist in the Str class 468 | * 469 | * @example 470 | * // Convert the field value to camel case 471 | * $field->str('camel'); 472 | * 473 | * // Adds -1 to the field's value or increment the ending number to allow -2, -3, etc. 474 | * $field->str('increment'); 475 | * 476 | * @link https://getkirby.com/docs/reference/objects/toolkit/str Kirby Str method overview 477 | */ 478 | public static function str(Field $field, string $method, ...$args): Field 479 | { 480 | if (!method_exists(Str::class, $method)) { 481 | throw new InvalidArgumentException("Method '$method' does not exist in Str class."); 482 | } 483 | 484 | $result = Str::$method($field->value(), ...$args); 485 | 486 | $field->value = $result; 487 | return $field; 488 | } 489 | 490 | /** 491 | * Dumps the field's value for debugging and returns the field. 492 | * 493 | * This method is a wrapper around Kirby's dump() method. It dumps the field's 494 | * current value and returns the field to allow for further method chaining. The 495 | * dumped value can be controlled via optional parameters. 496 | * 497 | * @param string $msg Optional debugging message that will be added to the dump output. 498 | * Use {{ val }} as placeholder for the value, or the string will be used as prefix 499 | * @param bool $echo Whether to echo the dump (true) or return it as the field's new value (false) 500 | * @param bool $dumpField If set to true will dump the field itself instead of its value 501 | * @param mixed $when A condition that determines whether to dump the output; defaults to true 502 | * 503 | * @return Field The original field when echoing the dump, otherwise the modified 504 | * field with the dump output as its value 505 | * 506 | */ 507 | public static function dump(Field $field, ?string $msg = null, bool $echo = true, bool $dumpField = false, mixed $when = true): Field 508 | { 509 | if (!self::isValidCondition($when)) return $field; 510 | 511 | $val = $field->value(); 512 | $dumpVal = $dumpField ? $field : $val; 513 | 514 | if ($msg !== null) { 515 | $fieldOutput = $dumpField ? trim(print_r($field, true)) : $val; 516 | $formattedMsg = preg_match('/{{[ ]*val[ ]*}}/', $msg) ? 517 | Str::template($msg, ['val' => $fieldOutput]) : 518 | $msg . $fieldOutput; 519 | 520 | dump($formattedMsg); 521 | } else { 522 | dump($dumpVal); 523 | } 524 | 525 | if (!$echo) { 526 | return $field->value(dump($dumpVal, false)); 527 | } 528 | 529 | return $field; 530 | } 531 | 532 | /** 533 | * Logs the field's value to a log file and returns the field. 534 | * 535 | * Creates a timestamped log entry in the site/logs directory. If the directory 536 | * doesn't exist, it will be created. Each log entry includes a timestamp and 537 | * the field's value, optionally wrapped in a custom message. 538 | * 539 | * @param string $msg Optional message for the log entry. Use {{ val }} as placeholder 540 | * for the value, or the string will be used as prefix 541 | * @param string $filename Name of the log file without extension (default: 'field_composer'). 542 | * If the file already exists, the log entry will be appended to the file. 543 | * @param bool $logField If set to true will log the field itself in Kirby's dump() format 544 | * instead of the field's value 545 | * @param mixed $when A condition that determines whether to log the output; defaults to true 546 | * 547 | * @return Field The original field, allowing for method chaining 548 | */ 549 | public static function log(Field $field, ?string $msg = null, string $filename = 'field_composer', bool $logField = false, mixed $when = true): Field 550 | { 551 | if (!self::isValidCondition($when)) return $field; 552 | 553 | $val = $logField ? trim(print_r($field, true)) : $field->value(); 554 | if ($msg !== null) { 555 | $val = preg_match('/{{[ ]*val[ ]*}}/', $msg) ? Str::template($msg, ['val' => $val]) : $msg . $val; 556 | } 557 | 558 | $time = date('Y-m-d H:i:s'); 559 | $log = "[$time] $val" . PHP_EOL; 560 | 561 | $dir = kirby()->root('site') . '/logs'; 562 | if (!Dir::exists($dir)) { 563 | Dir::make($dir); 564 | } 565 | 566 | $filepath = $dir . '/' . $filename . '.log'; 567 | F::write($filepath, $log, true); 568 | 569 | return $field; 570 | } 571 | 572 | private static function isValidCondition($condition): bool 573 | { 574 | return !($condition instanceof Field && $condition->isEmpty()) && 575 | $condition !== false && 576 | $condition !== null && 577 | $condition !== '' && 578 | $condition !== []; 579 | } 580 | 581 | private static function getFieldItems(Field $field, string|bool|null $split = null, ?Closure $callback = null): array 582 | { 583 | $items = null; 584 | 585 | if (is_string($split)) { 586 | // String split parameter - explicit split 587 | $items = Str::split($field->value(), $split); 588 | if ($callback) { 589 | $items = array_map($callback, $items); 590 | } 591 | } 592 | else { 593 | // Otherwise try array handling first 594 | $type = self::arrayFieldType($field); 595 | if ($type) { 596 | $items = match ($type) { 597 | 'pages' => $callback ? $field->toPages()->toArray($callback) : $field->toPages()->keys(), 598 | 'files' => $callback ? $field->toFiles()->toArray($callback) : $field->toFiles()->keys(), 599 | 'users' => $callback ? $field->toUsers()->toArray($callback) : $field->toUsers()->keys(), 600 | 'blocks' => $field->toBlocks()->toArray($callback), 601 | 'structure' => $field->toStructure()->toArray($callback), 602 | default => $field->toData('yaml') 603 | }; 604 | } 605 | // If no array type found and split is false, treat as single item 606 | elseif ($split === false) { 607 | $value = $callback ? $callback($field->value()) : $field->value(); 608 | $items = [$value]; 609 | } 610 | // Otherwise fall back to default split 611 | else { 612 | $items = Str::split($field->value(), ','); 613 | if ($callback) { 614 | $items = array_map($callback, $items); 615 | } 616 | } 617 | } 618 | 619 | // Convert any remaining sub-arrays to JSON 620 | $items = array_map(function($item) { 621 | return is_array($item) ? json_encode($item) : $item; 622 | }, $items); 623 | 624 | return array_values(array_filter($items, function ($item) { 625 | if($item instanceof Field) { 626 | return $item->toString() !== ''; 627 | } 628 | if(is_bool($item)) { 629 | return $item === true; 630 | } 631 | return $item !== ''; 632 | })); 633 | } 634 | 635 | private static function arrayFieldType(Field $field): string|null { 636 | $value = trim($field->value()); 637 | $isYaml = Str::startsWith($value, '- '); 638 | $isJson = preg_match('/^\[\s*{/', $value) === 1; 639 | 640 | if (!$isYaml && !$isJson) { 641 | return null; 642 | } 643 | 644 | $fieldDef = $field->parent()?->blueprint()?->field($field->key()); 645 | 646 | if ($fieldDef && in_array($fieldDef['type'], [ 647 | 'pages', 648 | 'files', 649 | 'users', 650 | 'structure', 651 | 'blocks', 652 | 'layout' 653 | ])) { 654 | return $fieldDef['type']; 655 | } 656 | 657 | 658 | try { 659 | if ($field->toPages()->count() > 0) return 'pages'; 660 | if ($field->toFiles()->count() > 0) return 'files'; 661 | if ($field->toUsers()->count() > 0) return 'users'; 662 | $data = Data::decode($value, 'yaml'); 663 | if (is_array($data) && A::isAssociative($data[0] ?? null)) { 664 | return 'structure'; 665 | } 666 | } catch (\Throwable $e) { 667 | // no structure field, continue 668 | } 669 | 670 | $blocks = $field->toBlocks(); 671 | if ($blocks->count() > 0 && !empty($blocks->first()->type())) { 672 | return 'blocks'; 673 | } 674 | 675 | if ($field->toLayouts()->count() > 0) return 'layout'; 676 | 677 | return null; 678 | } 679 | 680 | private static function addAffix(Field $field, Field|string $affix, string $separator, bool $isPrefix): Field 681 | { 682 | if ($field->isEmpty()) return $field; 683 | 684 | $affixValue = $affix instanceof Field ? $affix->value() : $affix; 685 | 686 | // return original field if affix is empty to not insert an extra separator 687 | if (empty($affixValue)) { 688 | return $field; 689 | } 690 | 691 | $mergedValue = $isPrefix 692 | ? $affixValue . $separator . $field->value() 693 | : $field->value() . $separator . $affixValue; 694 | 695 | return $field->value($mergedValue); 696 | } 697 | 698 | } 699 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kirby Field Composer 2 | 3 | Kirby Field Composer is a plugin that simplifies complex field operations in Kirby. It provides methods for merging fields, applying conditional logic and manipulating strings, handling field values intelligently to avoid unwanted formatting issues. This makes it easier to work with both simple and complex content structures. 4 | 5 | ## Features 6 | 7 | - 🧪 **Field Methods**: A collection of methods to manipulate and compose field values. 8 | - 🌐 **Global Helper Functions**: `field()` and `f()` for easy field composition. 9 | - 🧬 **Flexible Merging**: Combining multiple fields with custom separators and positioning. 10 | - 🏷️ **Smart Handling of Empty Fields:** No separators get inserted when fields are empty. 11 | - 🚦 **Conditional Field Handling**: Apply conditions to field rendering. 12 | - 📋 **List Methods**: Format fields to lists with powerful processing options 13 | - 🔡 **String Manipulation**: Apply Kirby's `Str` class methods directly to fields. 14 | - 🔍 **Debugging Tools**: Methods for logging and debugging complex field method chains. 15 | 16 | ## Table of Contents 17 | 18 | 19 | * [Overview](#overview) 20 | * [Installation](#installation) 21 | * [Field Methods](#field-methods) 22 | * [merge()](#field-mergeargs) 23 | * [prefix(), suffix()](#field-prefixprefix-separator-when) 24 | * [wrap(), tag()](#field-wrapbefore-after-separator-when) 25 | * [when(), whenAny(), notWhen(), notWhenAny(), whenAll(), whenNone()](#field-whenconditions) 26 | * [match()](#field-matchconditions-when) 27 | * [format()](#field-formatcallback) 28 | * [list(), count()](#field-listsplit-join-conjunction-serial-each-all-when) 29 | * [str()](#field-strmethod-args) 30 | * [dump(), log()](#field-dumpmsg-echo-dumpfield) 31 | * [Helpers](#helpers) 32 | * [field(), f()](#fieldargs) 33 | * [Options](#options) 34 | 35 | 36 | ## Overview 37 | Simple use cases include merging multiple fields to a single field’s value … 38 | ```php 39 | $page->title()->merge($page->author(), $page->year()); 40 | // => Chasing Starlight, Jane Doe, 2008 41 | ``` 42 | 43 | … conditionally prefixing fields with certain values … 44 | ```php 45 | $page->publisher()->prefix('Publisher: '); 46 | // => Publisher: Kirby Press 47 | ``` 48 | 49 | … or many more. But to understand how the plugin can become really useful, it’s helpful to look at a complex example: Let’s say on our website we have to display digital museum labels for a collection of paintings. We might need to compose a string after a schema like this: 50 | 51 | **{Artist Name}, {Year of Birth}-{Year of Death}, {Birth Place}; {Title of the Artwork}, {Year of Creation}; {Material} ({Width} × {Height} cm); {Collection}; {Description}** 52 | 53 | At first this might seem straight forward, but it can quickly become complex when you get into the specifics: there are sub-groups separated by semi-colons, while the sub-group entries themselves are separated by commas, mostly. When data is missing, it should not leave an abandoned separator in place, if the width is not given, the height should not display either, if the title is empty, it should be replaced by *Untitled*, if the artist is still alive, there should be a `*` before their year of birth and so on. 54 | 55 | Usually this would require a lot of fiddling with conditional statements, implode commands etc. 56 | 57 | The plugin offers methods to make this process significantly simpler. Here is how the code could look, making use of some of the plugin’s field methods: 58 | 59 | ```php 60 | // assuming we have two variables $artwork and $artist holding content on both 61 | field( 62 | [ 63 | $artist->name()->or('Unknown'), 64 | field($artist->born(), $artist->died(), '-') 65 | ->prefix('*', when: $artist->died()->isEmpty()), 66 | $artist->birthplace() 67 | ], 68 | [ 69 | $artwork->title()->or('Untitled'), 70 | $artwork->year() 71 | ], 72 | [ 73 | $artwork->material(), 74 | $artwork->width()->merge($artwork->height(), ' × ') 75 | ->when($artwork->width(), $artwork->height()) 76 | ->suffix(' cm') 77 | ->wrap('(', ')'), 78 | '' 79 | ], 80 | $artwork->collection(), 81 | $artwork->description(), 82 | '; ' 83 | ); 84 | ``` 85 | 86 | The result might look something like this: 87 | 88 | **Edward McDoe, 1856-1936, Scotland; Solitude, 1881; Oil on canvas (56 × 82 cm); Summerfield Collection; An impressionistic depiction of a lone farmer in the fields.** 89 | 90 | As this setup will flexibly handle empty fields, for another content file, where the artwork’s title, the dimensions and the collection are missing and the artist is still alive, it might result in something like this instead: 91 | 92 | **Jil Nash ,\*1982, Ireland; Untitled, 1994; Acrylic on wood; An abstract color explosion.** 93 | 94 | Additionally we could wrap fields into tags for styling, change fields conditionally etc. See below for a detailed list of available field methods. 95 | 96 | ## Installation 97 | 98 | ### Download 99 | 100 | Download and copy this repository to `/site/plugins/field-composer`. 101 | 102 | ### Git submodule 103 | 104 | ``` 105 | git submodule add https://github.com/trych/kirby-field-composer.git site/plugins/field-composer 106 | ``` 107 | 108 | ### Composer 109 | 110 | ``` 111 | composer require trych/kirby-field-composer 112 | ``` 113 | 114 | 115 | ## Usage 116 | 117 | When looking at the field methods, let's assume we have a page describing a painting with this content: 118 | ``` 119 | Title: Haze 120 | ---- 121 | Artist: Jil Nash 122 | ---- 123 | Year: 2014 124 | ---- 125 | Width: 48.2 126 | ---- 127 | Height: 67 128 | ---- 129 | Depth: 130 | ---- 131 | Description: Faint shapes lost in mist. 132 | ---- 133 | Info: 134 | ---- 135 | Museum: Tate 136 | ``` 137 | 138 | ## Field Methods 139 | 140 | Each of the plugin's field methods returns a field, so the methods can all be used to chain multiple field methods. 141 | 142 | ### `$field->merge(...$args)` 143 | 144 | Merges the field's value with the given arguments. The `merge()` method is the heart of the plugin and allows for complex composing of multiple fields and strings. 145 | 146 | - **`$args`:** one or multiple arguments (fields, strings, numbers, arrays) that will be merged to a single field's value. 147 | 148 | In its most simple form, it can merge the value of the original field with one or more given arguments. The default separator is `, `. 149 | ```php 150 | $page->title()->merge($page->year()); 151 | // => Haze, 2014 152 | ``` 153 | 154 | Further field methods can still be chained to the `merge()` method. 155 | ```php 156 | $page->title()->merge($page->artist())->upper(); 157 | // => HAZE, JIL NASH 158 | ``` 159 | 160 | Strings can be merged as well. 161 | ```php 162 | $page->title()->merge($page->artist(), 'Oil on canvas', $page->year()); 163 | // => Haze, Jil Nash, Oil on canvas, 2014 164 | ``` 165 | 166 | Empty fields will simply be omitted, without introducing duplicate separators. 167 | ```php 168 | $page->title()->merge($page->artist(), $page->info(), $page->year()); 169 | // => Haze, Jil Nash, 2014 170 | ``` 171 | 172 | If a string is used as the last argument, it will be interpreted as the separator to place between the separate parts. Otherwise the default separator (`', '` or the one set via the `mergeSeparator` option) will be used. 173 | ```php 174 | $page->title()->merge($page->artist(), $page->year(), ' / '); 175 | // => Haze / Jil Nash / 2014 176 | ``` 177 | 178 | If you want to merge a string as the last argument, remember to explicitly set the separator even if it matches the default separator, otherwise the last string to merge would be interpreted as separator. 179 | ```php 180 | // 🚫 this will use the string 'Sold' as a separator 181 | $page->title()->merge($page->artist(), $page->year(), 'Sold'); 182 | // => HazeSoldJil NashSold2014 183 | 184 | // ✅ pass the separator explicitly as the last argument instead 185 | $page->title()->merge($page->artist(), $page->year(), 'Sold', ', '); 186 | // => Haze, Jil Nash, 2014, Sold 187 | ``` 188 | 189 | If the original field's value should not be merged at the first position, an integer can be used as the last argument to specify the position at which the value should merge. 190 | ```php 191 | $page->title()->upper()->merge($page->year(), $page->artist(), $page->museum(), 2); 192 | // => 2014, Jil Nash, HAZE, Tate 193 | ``` 194 | 195 | Negative integers can be used as well, to count from the back of the list. 196 | ```php 197 | $page->title()->upper()->merge($page->year(), $page->artist(), $page->museum(), -1); 198 | // => 2014, Jil Nash, Tate, HAZE 199 | ``` 200 | 201 | If the last argument is set to `false`, the original value will not be merged at all, instead only the given arguments will be merged. This can be useful in more complex scenarios where the original value is part of a „sub-group“ within the string (see the `merge()` method’s use with arrays below). 202 | ```php 203 | $page->title()->merge($page->year(), $page->artist(), $page->museum(), false); 204 | // => 2014, Jil Nash, Tate 205 | ``` 206 | 207 | If the last argument is used to specify the position, the separator string can be provided as the *second to last* argument. 208 | ```php 209 | $page->title()->upper()->merge($page->artist(), $page->year(), $page->museum(), ' / ', 2); 210 | // => Jil Nash / 2014 / HAZE / Tate 211 | ``` 212 | 213 | If an array is provided as one of the arguments, its entries will be merged by the same rules, except that there is no original field value that is passed and therefore there is also no position option. This allows for complex merging when there are several "sub-groups" in the resulting string that might have different separators. 214 | ```php 215 | $page->title()->upper()->merge( 216 | [$page->artist(), $page->year()], // arguments will be merged separated by the default by ', ' 217 | [$page->description(), $page->info(), $page->museum(), ' | '], // arguments will be merged separated by ' | ' 218 | 'Sold', 219 | '; ' // separator, top level arguments will be merged separated by `; ` 220 | ); 221 | // => HAZE; Jil Nash, 2014; Faint shapes lost in mist | Tate; Sold 222 | ``` 223 | 224 | ### `$field->prefix($prefix, $separator, $when)` 225 | 226 | Adds a prefix to the field's value. If the field is empty or the condition is not met, no prefix is added. If an empty field is passed as the prefix, there will be no prefix and no separator added, so the field keeps its original value. 227 | 228 | - **`$prefix`:** The prefix to add (can be a Field or a string). 229 | - **`$separator`:** Optional separator between the prefix and the field value. 230 | - **`$when`:** Optional condition that determines whether to add the prefix. Default is `true`. 231 | 232 | ```php 233 | $page->title()->prefix('Title: '); 234 | // => Title: Haze 235 | 236 | $page->info()->prefix('Additional info: '); 237 | // => [returns an empty field, as the info field is also empty] 238 | 239 | $page->title()->prefix($page->artist(), ': '); 240 | // => Jil Nash: Haze 241 | 242 | $artist->born()->prefix('*', '', $artist->died()->isEmpty()); 243 | // => *1982 244 | 245 | // if you do not like to pass redundant arguments or like to be explicit 246 | // you can also pass named arguments 247 | $artist->born()->prefix('*', when: $artist->died()->isEmpty()); 248 | // => *1982 249 | ``` 250 | 251 | ### `$field->suffix($suffix, $separator, $when)` 252 | 253 | Adds a suffix to the field's value. If the field is empty or the condition is not met, no suffix is added. If an empty field is passed as the suffix, there will be no suffix and no separator added, so the field keeps its original value. 254 | 255 | - **`$suffix`:** The suffix to add (can be a Field or a string). 256 | - **`$separator`:** Optional separator between the field value and the suffix. 257 | - **`$when`:** Optional condition that determines whether to add the suffix. Default is `true`. 258 | 259 | ```php 260 | $page->width()->suffix(' cm'); 261 | // => 48.2 cm 262 | ``` 263 | 264 | ```php 265 | $page->width()->merge($page->height(), $page->depth(), ' × ') 266 | ->prefix('Dimensions: ') 267 | ->suffix(' cm'); 268 | // => Dimensions: 48.2 × 67 cm 269 | ``` 270 | In the above example, if all of the fields `width`, `height`, `depth` were empty, the `merge` would result in an empty field and neither the `prefix` nor the `suffix` values would be applied. 271 | 272 | ### `$field->wrap($before, $after, $separator, $when)` 273 | 274 | Wraps the field's value with specified strings or field values. If the field is empty or the condition is not met, no wrapping strings will be added. 275 | 276 | - **`$before`:** The string or field to prepend to the field's value. 277 | - **`$after`:** The string or field to append to the field's value. If null, `$before` is used. 278 | - **`$separator`:** An optional separator between the field value and the wrapping strings. 279 | - **`$when`:** Optional condition that determines whether to wrap the field. Default is `true`. 280 | 281 | ```php 282 | $page->title()->wrap('»', '«'); 283 | // => »Haze« 284 | ``` 285 | 286 | If an empty field is passed to `before` or `after`, there is no string prepended / appended and no separator inserted. 287 | ```php 288 | $page->artist()->wrap($page->title(), $page->info(), ' | '); 289 | // => Haze | Jil Nash 290 | ``` 291 | 292 | ### `$field->tag($tag, $attr, $indent, $level, $encode, $when)` 293 | 294 | Wraps the field's value in an HTML tag. If the field is empty or the condition is not met, no tags are added. 295 | 296 | - **`$tag`:** The HTML tag to wrap the field's value in. 297 | - **`$attr`:** An associative array of HTML attributes for the tag. 298 | - **`$indent`:** The indentation string, or null for no indentation. 299 | - **`$level`:** The indentation level. Defaults to `0`. 300 | - **`$encode`:** If `true` (default), encodes HTML in content for security. Set to `false` for outer tags in nested tag calls to preserve inner HTML structure. 301 | - **`$when`:** Optional condition that determines whether to wrap the field in a tag. Default is `true`. 302 | 303 | ```php 304 | $page->title()->tag('h1'); 305 | // =>

Haze

306 | 307 | $page->description()->tag('p', ['class' => 'description']); 308 | // =>

Faint shapes lost in mist.

309 | ``` 310 | 311 | When nesting multiple `tag()` calls like `$field->tag('em')->tag('p')`, the inner HTML tags will be encoded and shown as text rather than rendered as HTML. This happens because each `tag()` call encodes its content for security. To properly nest tags while maintaining security, you need to: encode user content with a regular `tag()` call (`encode: true`) and then for subsequent `tag()` calls set `encode: false` to preserve the HTML structure. 312 | 313 | ```php 314 | // 🚫 Incorrect output: inner tags will be encoded and shown as text 315 | $page->artist()->tag('em')->tag('p'); 316 | 317 | // 🚫 Insecure: encoding is disabled on both `tag()` calls. 318 | // The artist field's content is not sanitized. 319 | $page->artist()->tag('em', encode: false) 320 | ->tag('p', encode: false); 321 | 322 | // ✅ Secure: First tag encodes content, outer tag preserves HTML 323 | $page->artist()->tag('em') // inner tag encodes user content 324 | ->tag('p', encode: false); // outer tag preserves HTML 325 | 326 | // 🚫 Insecure: Even though inner tag encodes initial content, 327 | // merging additional content between tag calls breaks the security chain 328 | $page->artist()->tag('em') // this encodes artist content 329 | ->merge($page->description()) // description content is raw 330 | ->tag('p', encode: false); // preserves HTML, but now includes unencoded content 331 | 332 | // ✅ Secure: As long as the tags are only wrapped around already secured content, 333 | // they can be chained infinitely without compromising security since the 334 | // initial encoding protects all user content 335 | $page->artist()->tag('em') // sanitizes content 336 | ->tag('strong', encode: false) // preserves HTML 337 | ->tag('p', encode: false); // preserves HTML 338 | ``` 339 | 340 | ### `$field->when(...$conditions)` 341 | 342 | Returns the original field if all conditions are valid, otherwise returns an empty field. If a field is passed as one of the conditions, it evaluates to `false` in case it is empty. 343 | 344 | - **`$conditions`:** Variable number of conditions to check. 345 | 346 | ```php 347 | // just pass the dimensions, if both the `width` and the `height` are given 348 | $page->width()->merge($page->height(), ' × ')->suffix(' cm') 349 | ->when($page->width(), $page->height()); 350 | // => 48.2 × 67 cm 351 | ``` 352 | 353 | ### `$field->whenAny(...$conditions)` 354 | 355 | Returns the original field if any of the conditions is valid, otherwise returns an empty field. If a field is passed as one of the conditions, it evaluates to `false` in case it is empty. 356 | 357 | - **`$conditions`:** Variable number of conditions to check. 358 | 359 | ```php 360 | // just pass the museum, if either `artist` or `info` are given 361 | $page->museum()->prefix('Gallery: ', '')->whenAny($page->artist(), $page->info()); 362 | // => Gallery: Tate 363 | ``` 364 | 365 | ### `$field->notWhen(...$conditions)` 366 | 367 | Returns an empty field if all conditions are valid, otherwise returns the original field. If a field is passed as one of the conditions, it evaluates to `false` in case it is empty. 368 | 369 | - **`$conditions`:** Variable number of conditions to check. 370 | 371 | ```php 372 | // shows the `description` only if `info` is empty 373 | $page->description()->notWhen($page->info()); 374 | // => Faint shapes lost in mist. 375 | ``` 376 | 377 | ### `$field->notWhenAny(...$conditions)` 378 | 379 | Returns an empty field if any of the conditions are valid, otherwise returns the original field. If a field is passed as one of the conditions, it evaluates to `false` in case it is empty. 380 | 381 | - **`$conditions`:** Variable number of conditions to check. 382 | 383 | ```php 384 | // do not pass museum if either `artist` or `info` are given 385 | $page->museum()->notWhenAny($page->artist(), $page->info()); 386 | // => [empty, as `artist` is given] 387 | ``` 388 | 389 | ### `$field->whenAll(...$conditions)` 390 | 391 | Alias for `when()`. Returns the field if all conditions are valid. 392 | 393 | ### `$field->whenNone(...$conditions)` 394 | 395 | Alias for `notWhenAny()`. Returns the field if none of the conditions are valid. 396 | 397 | ### `$field->match($conditions, $when)` 398 | 399 | Similar to [PHP's match expression](https://www.php.net/manual/en/control-structures.match.php), this matches the field's value against the keys of an array of key/value pairs and returns their corresponding values if a match is found. In case no match is found, the original field is returned. Alternatively, setting `'default'` as the last key in the array provides a fallback value for unmatched cases. 400 | 401 | - **`$conditions`:** Array of key/value pairs where the keys are matched against the field's value 402 | - **`$when`:** Optional condition that determines whether to run the matching operation. If the condition is not met, the original field is returned unchanged. Default is `true`. 403 | 404 | ```php 405 | // Basic matching with fallback 406 | $page->museum()->match([ 407 | 'Tate' => 'Tate Gallery', 408 | 'MoMA' => 'Museum of Modern Art', 409 | 'Louvre' => 'Musée du Louvre', 410 | 'default' => 'Unknown gallery' 411 | ]); 412 | // => 'Tate Gallery' 413 | ``` 414 | 415 | ### `$field->format($callback)` 416 | 417 | Applies a custom formatting function to the field's value. 418 | 419 | This is very similar to [Kirby’s native $field->callback\(\) method](https://getkirby.com/docs/reference/templates/field-methods/callback), except that for convenience the field’s value is used as the first parameter of the callback function (with the field itself being the second one) and only a string needs to be returned, the re-wrapping into a field happens automatically. Returning the field with the new value directly will also work, though. 420 | 421 | - **`$callback`:** A closure that takes the field's value and the field object as arguments and returns the new formatted value. The value will be automatically wrapped in a field again. 422 | 423 | ```php 424 | // remove all vowels from a string 425 | $page->description()->format(function($value) { 426 | return preg_replace('/[aeiou]/i', '', $value); 427 | }); 428 | // => Fnt shps lst n mst. 429 | ``` 430 | 431 | ### `$field->list($split, $join, $conjunction, $serial, $each, $all, $when)` 432 | 433 | Converts a field's value into a formatted list with advanced processing options. This method can handle any field type that represents a list: strings (with custom separators), structure fields, pages fields, files fields, users fields or blocks fields. The method provides options to format the output with custom separators and conjunctions, process individual items and transform the entire list. 434 | 435 | - **`$split`:** Pattern to split string value, `null` for auto-detect, `false` to force array handling (non-array fields will be treated as single item) 436 | - **`$join`:** String to join list items. Defaults to `, ` or the user-defined `listJoinSeparator` option 437 | - **`$conjunction`:** Optional conjunction text or callback before last item. Defaults to no conjunction or the user-configured `listConjunction` option 438 | - **`$serial`:** Whether to use serial (Oxford) comma before conjunction. Defaults to `false` 439 | - **`$each`:** Optional callback to process each item 440 | - **`$all`:** Optional callback to process the entire list array right before formatting it to a list 441 | - **`$when`:** Optional condition that determines whether to process the field. Default is `true` 442 | 443 | In its most basic form, it converts a comma-separated string into a formatted list: 444 | ```php 445 | // Simple list from comma-separated string 446 | $page->keywords()->list(','); 447 | // => red, blue, green 448 | ``` 449 | 450 | The output format can be customized using the parameters `$join`, `$conjunction` and `$serial`. The `$join` parameter sets the separator between items in the resulting list, while `$conjunction` adds text before the last item. Setting `$serial` to `true` adds an Oxford comma before the conjunction: 451 | ```php 452 | // Custom join separator 453 | $page->keywords()->list(',', '|'); 454 | // => red|blue|green 455 | 456 | // List with conjunction 457 | $page->keywords()->list(',', null, 'or')->upper(); 458 | // => RED, BLUE OR GREEN 459 | 460 | // List with conjunction and Oxford comma 461 | $page->keywords()->list(',', null, 'and', true); 462 | // => red, blue, and green 463 | ``` 464 | 465 | The method automatically handles Kirby's list-type fields like pages, files, users, blocks and structure fields. Using the `$each` callback, you can process each item before it gets added to the list. The items are the individual collection items of the given field type. That means a pages field will be converted to page objects, so all page methods can be used in the item callback and accordingly for other collection types. 466 | ```php 467 | // Splitting a files field and listing the file names by using a callback 468 | $page->slideshow()->list( 469 | each: fn($img) => $img->filename() . ' (' . $img->dimensions() . ' px)' 470 | ); 471 | // => photo1.jpg (720 × 640 px), photo2.webp (600 × 400 px), photo3.jpg (1280 × 720 px) 472 | 473 | // If `false` or an empty string `''` is returned for an item, this item does not get listed 474 | $page->slideshow()->list( 475 | each: fn($img) => $img->extension() === 'jpg' ? $img->filename() : false; 476 | ); 477 | // => photo1.jpg, photo3.jpg 478 | 479 | // List structure field values 480 | $page->team()->list(each: fn($member) => $member->name()); 481 | // => John Doe, Jane Smith, Alex Johnson 482 | 483 | // List block types 484 | $page->blocks()->list(each: fn($block) => $block->type()); 485 | // => text, gallery, text, quote, image, text 486 | ``` 487 | 488 | The `$all` callback allows you to transform the entire list before it gets formatted. This is useful for sorting, filtering or removing duplicates: 489 | ```php 490 | // Sort items alphabetically before joining 491 | $page->tags()->list( 492 | each: fn($tag) => $tag->name(), 493 | all: fn($items) => sort($items) 494 | ); 495 | // => art, culture, design, photography 496 | 497 | // Outputting all types used in a blocks field with unique, sorted values 498 | $page->article()->list( 499 | each: fn($item) => $item->type(), 500 | all: fn($types) => sort(array_unique($types)) 501 | ); 502 | // => gallery, image, quote, text 503 | ``` 504 | 505 | ### `$field->count($split, $each, $when)` 506 | 507 | Counts the number of items in a field that represents a list. Works with any field type that can be interpreted as a list: structure fields, pages fields, files fields, users fields, blocks fields or strings with a user defined separator. If an `$each` callback is provided, strings or booleans can be returned. Empty strings or `false` values are not counted in this case. The `$all` callback allows you to transform the entire list before it will result in a count. This is useful for further filtering, removing duplicates etc. 508 | 509 | - **`$split`:** Pattern to split string value, `null` for auto-detect, `false` to force array handling (non-array fields will be treated as single item) 510 | - **`$each`:** Optional callback to process each item before counting. Can return transformed values or booleans 511 | - **`$all`:** Optional callback to process the entire list array right before converting it to the final count 512 | - **`$when`:** Optional condition that determines whether to process the field. Default is `true` 513 | 514 | ```php 515 | // Count items in a simple comma-separated field 516 | $page->keywords()->count(); 517 | // => 3 518 | 519 | // Count items in a structure field and count only the items 520 | // that have an entry in the `street` column 521 | $page->addresses()->count(null, fn($address) => $address->zip() ); 522 | // => 12 (number of addresses with a given zip code) 523 | 524 | // Count images wider than 1000px in a slideshow field 525 | $page->slideshow()->count(null, fn($file) => $file->width() > 1000 ); 526 | // => 5 (number of images wider than 1000px) 527 | 528 | // Count unique types used in a blocks field 529 | $page->article()->list( 530 | each: fn($item) => $item->type(), 531 | all: fn($types) => array_unique($types) 532 | ); 533 | // => 4 (number of unique types) 534 | ``` 535 | 536 | ### `$field->str($method, ...$args)` 537 | 538 | Applies a [Kirby Str class method](https://getkirby.com/docs/reference/objects/toolkit/str) to the field's value. 539 | 540 | - **`$method`:** The name of the `Str` class method to apply. 541 | - **`$args`:** Additional arguments to pass to the Str method. 542 | 543 | ```php 544 | // Change the field's value to camel case 545 | $page->artist()->str('camel'); 546 | // => jilNash 547 | 548 | // Adds -1 to the field's value or increments the ending number to allow -2, -3, etc. 549 | $page->title()->lower()->str('increment'); 550 | // => haze-1 551 | ``` 552 | 553 | ### `$field->dump($msg, $echo, $dumpField)` 554 | 555 | Dumps the field's value for debugging and returns the field. This is a wrapper around Kirby's `dump()` method that maintains chainability. 556 | 557 | - **`$msg`:** Optional debugging message that will be added to the dump output. If the message includes the placeholder `{{ val }}`, the field's value will replace it, otherwise the message will be used as prefix. 558 | - **`$echo`:** Whether to echo the dump (`true`) or return it as the field's new value (`false`). Default is `true`. 559 | - **`$dumpField`:** If set to `true` will dump the field itself instead of its value. Default is `false`. 560 | 561 | ```php 562 | // Simple dump 563 | $page->artist()->dump(); 564 | 565 | // With prefix 566 | $page->artist()->dump('artist value: '); 567 | 568 | // With template 569 | $page->artist()->dump('The artist known as {{ val }}!!'); 570 | 571 | // Return dump result instead of echoing 572 | $page->artist()->dump('Artist: ', false)->upper(); 573 | // => "ARTIST: JIL NASH" 574 | 575 | // Dump entire field object 576 | $page->artist()->dump('artist field: ', true, true); 577 | ``` 578 | 579 | ### `$field->log($msg, $filename, $logField)` 580 | 581 | Logs the field's value to a log file and returns the field. Creates a timestamped log entry in the `site/logs` directory. Each log entry includes a timestamp and the field's value, optionally wrapped in a custom debugging message. This method is particularly useful when debugging field operations in contexts where output cannot be displayed, such as in Kirby Panel query strings or on production servers. 582 | 583 | - **`$msg`:** Optional debugging message for the log entry. If the message includes the placeholder `{{ val }}`, the field's value will replace it, otherwise the message will be used as prefix. 584 | - **`$filename`:** Name of the log file without extension (default: `'field_composer'`). If the file already exists, the log entry will be appended to the file. 585 | - **`$logField`:** If set to true will log the field itself in Kirby's `dump()` format instead of the field's value. Default is `false`. 586 | 587 | ```php 588 | // Simple log 589 | $page->artist()->log(); 590 | // => [2024-11-10 14:30:22] Jil Nash 591 | 592 | // With prefix 593 | $page->artist()->log('Artist: '); 594 | // => [2024-11-10 14:30:22] Artist: Jil Nash 595 | 596 | // With template 597 | $page->artist()->log('Found artist {{ val }} in page'); 598 | // => [2024-11-10 14:30:22] Found artist Jil Nash in page 599 | 600 | // Custom log file 601 | $page->artist()->log('Artist: ', 'artist_logs'); 602 | // => creates/appends to site/logs/artist_logs.log 603 | 604 | // Log entire field object 605 | $page->artist()->log('Artist field: ', 'field_logs', true); 606 | // => logs the full field object in dump format to site/logs/field_logs.log 607 | ``` 608 | 609 | # Helpers 610 | The plugin provides a global helper function `field()` along with a shortcut alias `f()`. 611 | 612 | ### `field(...$args)` 613 | The field helper allows you to compose a field from given values. This field can then be used to chain it with other field methods. The arguments work the same way as they do in the `$field->merge()` field method described above: You can pass fields, strings or numbers and they will be merged to the new field’s value. 614 | 615 | ```php 616 | field($page->title(), $page->artist(), 'sold', ', ')->upper() 617 | // => HAZE, JIL NASH, SOLD 618 | ``` 619 | 620 | If an array is passed, it will merge its values to a field by the same rules. If there is more than one argument and the last given argument is a string, it will be interpreted as a separator. Unlike the `$field->merge()` method, the last argument cannot be used as a position parameter as there is no initial field value that gets passed into the `field()` helper. 621 | 622 | The field helper is especially useful if you need to compose a field where the first value is part of a „sub-group“ or if you need to chain further field methods to such a sub-group, as shown in the example below. 623 | 624 | ```php 625 | field( 626 | [$page->title()->tag('em'), $page->year()], 627 | $page->artist(), 628 | field($page->width(), $page->height(), ' × ')->suffix(' cm') 629 | ->when($page->width(), $page->height()), 630 | $page->description()->prefix('Subject: '), 631 | $page->info()->prefix('Info: '), 632 | '; ' // separator for the top level 633 | ); 634 | // => Haze, 2014; Jil Nash; 48.2 × 67 cm; Subject: Faint shapes lost in mist. 635 | ``` 636 | 637 | ### `f(...$args)` 638 | Alias for `field()`. 639 | 640 | You can disable one or both helpers by setting their respective constant in `index.php` to `false` as [described in the Kirby helper docs](https://getkirby.com/docs/reference/templates/helpers#deactivate-a-helper-globally). 641 | 642 | ```php 643 | // /index.php 644 | 645 | render(); 653 | ``` 654 | # Options 655 | The plugin has four options, `mergeSeparator`, `affixSeparator`, `listJoinSeparator` and `listConjunction`. 656 | 657 | The `mergeSeparator` sets the default separator for the `$field->merge()` as well as the `field()` helper. Its default value is a comma followed by a space: `', '`. 658 | 659 | The `affixSeparator` sets the default separator for the field methods `$field->prefix()`, `$field->suffix()` and `$field->wrap()` ("affix" being the umbrella term for "prefix" and "suffix"). Its default value is an empty string: `''`. 660 | 661 | The `listJoinSeparator` sets the default separator between list items for the `$field->list()` method. Its default value is a comma followed by a space: `', '`. 662 | 663 | The `listConjunction` sets the default conjunction word for the `$field->list()` method. Its default value is `null` (no conjunction). It could be set to a simple string like `'and'` or for multilingual sites it could set to a callback that returns a translated conjunction: `fn() => t('and')`. 664 | 665 | You can change the defaults in your `config.php` file. 666 | 667 | ```php 668 | // /site/config/config.php 669 | 670 | return [ 671 | 'trych.field-composer' => [ 672 | 'mergeSeparator' => ' | ', 673 | 'affixSeparator' => ' ', 674 | 'listJoinSeparator' => '/', 675 | 'listConjunction' => fn() => t('and') // returns "and", "und", "et" etc. based on current language 676 | ] 677 | ]; 678 | ``` 679 | 680 | These user-defined options can still be overridden by providing explicit parameters in the method calls: 681 | ```php 682 | $page->title()->merge($page->artist(), $page->year(), ' / '); 683 | 684 | $page->title()->prefix($page->artist(), ': '); 685 | 686 | $page->keywords()->list(join: ' | '); 687 | 688 | $page->members()->list(conjunction: '&'); 689 | ``` 690 | 691 | --- 692 | ## Contributing 693 | If you encounter any issues or have suggestions, please [open an new issue](https://github.com/trych/kirby-field-composer/issues). 694 | ## License 695 | 696 | [MIT](./LICENSE) License © 2024 [Timo Rychert](https://github.com/trych) 697 | --------------------------------------------------------------------------------