├── .prettierignore ├── README.md ├── composer.json ├── phpunit.dist.xml └── src ├── Concerns └── UsesTransformer.php ├── Contracts └── Transformable.php ├── DataTransformer.php ├── Exceptions ├── AbortedTransformationException.php ├── ExecutionNotAllowedException.php └── NotCallableException.php ├── Transformer.php ├── TransformerRuleParser.php └── TransformerServiceProvider.php /.prettierignore: -------------------------------------------------------------------------------- 1 | composer.* 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transformer 2 | transformer is a PHP package for transforming values or input, powered by the [Laravel](https://laravel.com) framework's validation components. 3 | 4 | ![Tests](https://github.com/surgiie/transformer/actions/workflows/tests.yml/badge.svg) 5 | 6 | 7 | ## Installation 8 | ```bash 9 | composer require surgiie/transformer 10 | ```` 11 | 12 | ## Usage 13 | 14 | To use the package, pass your data and an array of callable functions that your data should be passed through: 15 | 16 | ```php 17 | ' jim ', 35 | 'last_name'=>' thompson', 36 | 'address' => '123 some street', 37 | 'phone_number'=>'123-456-7890', 38 | 'date_of_birth'=>"1991-05-01", 39 | ]; 40 | 41 | $transformers = [ 42 | 'first_name'=>'trim|ucfirst', 43 | 'last_name'=>'trim|ucfirst', 44 | 'phone_number'=>'only_numbers', 45 | // more on object values and method delegation below 46 | 'address' => [Stringable::class, '->after:123 ', '->toString'], 47 | 'date_of_birth'=>'to_carbon|->format:m/d/y', 48 | ]; 49 | 50 | $transformer = new DataTransformer($input, $transformers); 51 | 52 | $newData = $transformer->transform(); 53 | // Returns: 54 | // [ 55 | // "first_name" => "Jim", 56 | // "last_name" => "Thompson", 57 | // "phone_number" => "1234567890", 58 | // "address"=> "some street", 59 | // "date_of_birth" => "05/01/91", 60 | // ] 61 | 62 | ``` 63 | Note that the syntax is similar to the Laravel validation syntax because this package is powered by the same components. 64 | 65 | 66 | ## Passing Arguments 67 | 68 | You can specify arguments for your functions using a `:` syntax: 69 | 70 | ```php 71 | 'your_function:arg1,arg2', 75 | ]; 76 | 77 | ``` 78 | ### Specify Value Argument Order 79 | 80 | By default, the first argument passed to your function will be the value being formatted, followed by the arguments specified in the order provided. If your function does not accept the value as the first argument, you can use the `:value:` placeholder to specify the order. For example: 81 | 82 | ```php 83 | '123-456-3235']; 86 | $transformers = [ 87 | 'example'=>'preg_replace:/[^0-9]/,,:value:', 88 | ]; 89 | $transformer = new DataTransformer($input, $transformers); 90 | $transformer->transform(); 91 | ``` 92 | 93 | ### Casting Arguments 94 | 95 | You may find that when passing arguments to the transforming functions, you may hit run time errors due to typehints on arguments for basic types. Since arguments are being specified as a string here, you may come across a scenario like this: 96 | 97 | 98 | ```php 99 | "1"]; 106 | 107 | $transformers = [ 108 | 'example'=>'example_function:1', 109 | ]; 110 | 111 | ``` 112 | 113 | The above would throw an error because the `1` argument being passed in is a string and the function expects a integer. In these cases, you can use the following convention to cast the argument to specific native type, by suffixing `@` to the value. For example to cast the `1` to an integer: 114 | 115 | 116 | ```php 117 | "1"]; 124 | 125 | $transformers = [ 126 | 'example'=>'example_function:1@int', // "1" will be casted to an int. 127 | ]; 128 | 129 | ``` 130 | 131 | #### Available Casting Types: 132 | 133 | The following basic casting types are available 134 | 135 | - int 136 | - str 137 | - float 138 | - bool 139 | - array 140 | - object 141 | 142 | **Note** For more complex type or casting needs, use a Closure or `Surgiie\Transformer\Contracts\Transformable` class as documented below. 143 | 144 | ## Optional Transformation 145 | 146 | If you only want to transform a value if it is not null or "blank", you can use the `?` character in the chain of functions to specify when to break out of processing. This is often placed at the start of the chain: 147 | 148 | ```php 149 | 150 | null]; 153 | $transformers = [ 154 | 'example'=>'?|function_one|function_two', 155 | ]; 156 | $transformer = new DataTransformer($input, $transformers); 157 | $transformer->transform(); 158 | ``` 159 | Note: This package uses Laravel's `blank` helper to determine blank/empty values. If you have more complex logic for breaking out of rules, you can use a closure or a `\Surgiie\Transformer\Contracts\Transformable` class and call the 2nd argument exit callback. 160 | 161 | ## Closures and Transformable Classes 162 | 163 | You can use closures to transform your values: 164 | 165 | ```php 166 | 167 | ' Bob']; 170 | $transformers = [ 171 | 'first_name'=>['trim', function ($value) { 172 | // modify the value 173 | return $value; 174 | }], 175 | ]; 176 | $transformer = new DataTransformer($input, $transformers); 177 | $transformer->transform(); 178 | ``` 179 | Alternatively, you can implement the `Surgiie\Transformer\Contracts\Transformable` contract and use class instances: 180 | 181 | ```php 182 | 183 | ' Bob']; 205 | $transformers = [ 206 | 'first_name' => ['trim', new TransformValue], 207 | ]; 208 | $transformer = new DataTransformer($input, $transformers); 209 | $transformer->transform(); 210 | 211 | ``` 212 | 213 | ## Array Input 214 | You can also format nested array data using dot notation: 215 | 216 | ```php 217 | 218 | [ 222 | 'first_name'=>' jim ', 223 | 'last_name'=>' thompson', 224 | 'phone_number'=>'123-456-7890', 225 | 'address'=>'123 some lane.' 226 | ] 227 | ]; 228 | $transformers = [ 229 | 'contact_info.first_name'=>'trim|ucwords', 230 | 'contact_info.last_name'=>'trim|ucwords', 231 | 'contact_info.phone_number'=>'preg_replace:/[^0-9]/,,:value:', 232 | 'contact_info.address'=>[function ($address) { 233 | return 'Address Is: '.$address; 234 | }], 235 | ]; 236 | $transformer = new DataTransformer($input, $transformers); 237 | $transformer->transform(); 238 | ``` 239 | ## Wildcards 240 | You can also use wildcards on keys to apply transformers on keys that match the wildcard pattern: 241 | 242 | ```php 243 | 244 | ' jim ', 248 | 'last_name'=>' thompson', 249 | 'ignored'=>' i-will-be-the-same' 250 | ]; 251 | $transformers = [ 252 | // apply to all keys that contain "name" 253 | '*name*'=>'trim|ucwords', 254 | ]; 255 | $transformer = new DataTransformer($input, $transformers); 256 | $transformer->transform(); 257 | ``` 258 | 259 | ## Object Values/Method Delegation 260 | It is possible to delegate a function call to an object if the value has been converted to an instance. Using the syntax `->:args`, you can specify method chaining on that instance: 261 | 262 | ```php 263 | 264 | value = $value; 276 | } 277 | 278 | public function concat($string) 279 | { 280 | return $this->value . $string; 281 | } 282 | 283 | } 284 | 285 | function example($value) 286 | { 287 | return new Example($value); 288 | } 289 | 290 | 291 | $input = [ 292 | 'string'=>"Foo", 293 | ]; 294 | $transformers = [ 295 | 'string'=>'example|->concat:Bar', 296 | ]; 297 | ``` 298 | You can also use class constants that accept a single value as its constructor, for example: 299 | 300 | ```php 301 | 302 | "Foo", 306 | ]; 307 | $transformers = [ 308 | 'string'=>[Example::class, '->concat:Bar'], 309 | ]; 310 | ``` 311 | 312 | ## Guard Layer Over Execution 313 | 314 | By default, all available functions that are callable at runtime will be executed. However, if you want to add a protection or security layer that prevents certain methods from being called, you can add a guard callback that checks if a method should be called by returning true: 315 | 316 | ```php 317 | 318 | ' jim ', 331 | ]; 332 | $transformers = [ 333 | 'first_name'=>'trim|ucwords', 334 | ]; 335 | 336 | $transformer = new DataTransformer($input, $transformers); 337 | 338 | // throws a Surgiie\Transformer\Exceptions\ExecutionNotAllowedException once it gets to ucwords due to the guard method. 339 | $transformer->transform(); 340 | ``` 341 | 342 | ## Manually Transforming Values/Single Values 343 | To format a one-off value, use the Transformer class: 344 | 345 | 346 | ```php 347 | transform(); // returns "Uncle Bob" 354 | ``` 355 | 356 | ## Use Traits 357 | 358 | To transform data and values on-the-fly in your classes, use the `\Surgiie\Transformer\Concerns\UsesTransformer` trait: 359 | 360 | ```php 361 | 362 | transform(" example ", ['trim|ucwords']); 378 | // or transform an array of data 379 | $newData = $this->transformData(['example'=> 'data '], ["example"=>'trim|ucwords']); 380 | } 381 | } 382 | ``` 383 | ### Use the Request Macro 384 | To transform data using a macro on a `Illuminate\Http\Request` object instance, call the `transform()` method on the request, which returns the transformed data. 385 | 386 | 387 | ```php 388 | 389 | public function store(Request $request) 390 | { 391 | // Using data from the request object (i.e. `$request->all()`) 392 | $transformedData = $request->transform([ 393 | 'first_name' => ['strtoupper'], 394 | ]); 395 | 396 | // $transformedData['first_name'] will be all uppercase 397 | // all other data will be included from the request 398 | 399 | // You can also customize the input that is transformed, 400 | // in this case $transformedData will only have the `first_name` key. 401 | $transformedData = $request->transform($request->only(['first_name']), [ 402 | 'first_name' => ['strtoupper'], 403 | ]); 404 | } 405 | 406 | ``` 407 | When calling on a `FormRequest` object, it uses the `validated()` function to retrieve the input data. Note that this requires the data you are targeting to be defined as a validation rule in your form request's rules function, otherwise the data will be omitted from transformation. 408 | 409 | ## Package Discovery/Don't Discover 410 | Laravel automatically registers the package service provider, but if you don't want this, you can ignore package discovery for the service provider by including the following in your `composer.json`: 411 | 412 | ```json 413 | "extra": { 414 | "laravel": { 415 | "dont-discover": [ 416 | "surgiie/transformer" 417 | ] 418 | } 419 | } 420 | ``` 421 | 422 | 423 | ## Contribute 424 | 425 | Contributions are always welcome in the following manner: 426 | 427 | - Discussions 428 | - Issue Tracker 429 | - Pull Requests 430 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "surgiie/transformer", 3 | "description": "A data transforming/formatting package for php.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "php", 8 | "formatting", 9 | "input formatting", 10 | "sanitize", 11 | "sanitize input" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Sergio Compean", 16 | "email": "scompean24@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.2.0", 21 | "illuminate/http": "^11.0|^12.0", 22 | "illuminate/support": "^11.0|^12.0", 23 | "illuminate/validation": "^11.0|^12.0" 24 | }, 25 | "require-dev": { 26 | "pestphp/pest": "^2.34", 27 | "laravel/pint": "^1.16", 28 | "symfony/var-dumper": "^7.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Surgiie\\Transformer\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Surgiie\\Transformer\\Tests\\": "tests/" 38 | } 39 | }, 40 | "scripts": { 41 | "test": "./vendor/bin/pest tests -c phpunit.dist.xml" 42 | }, 43 | "config": { 44 | "allow-plugins": { 45 | "pestphp/pest-plugin": true 46 | } 47 | }, 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "Surgiie\\Transformer\\TransformerServiceProvider" 52 | ] 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /phpunit.dist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tests 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Concerns/UsesTransformer.php: -------------------------------------------------------------------------------- 1 | transformer($value, $functions, $name)->transform(); 26 | } 27 | 28 | /**Transform the given array data.*/ 29 | public function transformData(array $data = [], array $functions = []): array 30 | { 31 | return $this->dataTransformer($data, $functions)->transform(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Contracts/Transformable.php: -------------------------------------------------------------------------------- 1 | setData($data); 28 | $this->setFunctions($functions); 29 | } 30 | 31 | /**Set the data to transform.*/ 32 | public function setData(array $data) 33 | { 34 | $this->data = $data; 35 | } 36 | 37 | /**Set the transformers to apply on the data.*/ 38 | public function setFunctions(array $functions): static 39 | { 40 | $this->functions = $functions; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Create a new DataTransformer instance. 47 | */ 48 | public static function create($data, $callables): DataTransformer 49 | { 50 | return new static($data, $callables); 51 | } 52 | 53 | /** 54 | * Apply the given transformers to the given data key. 55 | */ 56 | protected function applyTransformers(Transformer $transformer, string $key, array $functions): static 57 | { 58 | if (! Arr::has($this->data, $key)) { 59 | return $this; 60 | } 61 | 62 | $transformer->setValue(Arr::get($this->data, $key)); 63 | $transformer->setFunctions($functions); 64 | 65 | Arr::set($this->data, $key, $transformer->transform()); 66 | 67 | return $this; 68 | } 69 | 70 | /**Get a new rule parser for transformers.*/ 71 | protected function parser(array $data) 72 | { 73 | return new TransformerRuleParser($data); 74 | } 75 | 76 | /** 77 | * Transform the set data and return it. 78 | */ 79 | public function transform(): array 80 | { 81 | $parsed = $this->parser($this->data)->explode($this->functions); 82 | 83 | $transformer = $this->transformer(); 84 | foreach ($parsed->rules as $key => $functions) { 85 | $this->applyTransformers($transformer, $key, $functions); 86 | } 87 | 88 | return $this->data; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Exceptions/AbortedTransformationException.php: -------------------------------------------------------------------------------- 1 | setValue($value); 37 | $this->setName($name); 38 | $this->setFunctions($functions); 39 | } 40 | 41 | /** 42 | * Call transformer function using the given value and parameters. 43 | */ 44 | protected function call($method, $value, array $args = []) 45 | { 46 | //first prepare the arguments for the method call. 47 | $args = $this->prepareArguments($value, $method, $args); 48 | 49 | // prep the method for execution if needed. 50 | $function = $this->prepareTransformerFunction($method); 51 | 52 | // check if the transformer method should be delegated to on an 53 | // underlying object this is done using a "->" convention. 54 | // this allows for transforming to be delegated to 3rd party classes such 55 | // as carbon. e.g to_carbon|->format:m/d/Y 56 | if ($this->shouldDelegateTransformer($value, $function)) { 57 | $function = ltrim($function, '->'); 58 | 59 | return $value->{$function}(...$args); 60 | } 61 | 62 | if (is_string($function) && class_exists($function)) { 63 | return new $function($value); 64 | } 65 | //check if its a custom transformable class 66 | if ($function instanceof Transformable) { 67 | return $function->transform($value, $this->abortTransformationCallback()); 68 | // or a callback 69 | } elseif ($function instanceof Closure) { 70 | return $function($value, $this->abortTransformationCallback()); 71 | } 72 | //otherwise check if the method is function is even callable/not guarded 73 | elseif (! is_callable($function)) { 74 | throw new NotCallableException( 75 | "Function $function not callable.", 76 | ); 77 | } elseif (! $this->shouldExecute($function)) { 78 | throw new ExecutionNotAllowedException("Function $function is not allowed to be called."); 79 | } 80 | 81 | return $function(...$args); 82 | } 83 | 84 | /**Set the name of the value/input being transformed.*/ 85 | protected function setName(?string $name = null): static 86 | { 87 | $this->name = $name; 88 | 89 | return $this; 90 | } 91 | 92 | /* 93 | * Check if the given transformer is allowed and not guarded. 94 | */ 95 | protected function shouldExecute($method): bool 96 | { 97 | if ($method instanceof Transformable || $method instanceof Closure) { 98 | return true; 99 | } 100 | 101 | $guard = Transformer::getGuardWith(); 102 | 103 | return filter_var($guard($method, $this->value, $this->name), FILTER_VALIDATE_BOOL); 104 | } 105 | 106 | /** 107 | * Register the callback that determines execution of transformers. 108 | */ 109 | public static function guard(Closure $callback): void 110 | { 111 | static::$guardWith = $callback; 112 | } 113 | 114 | /** 115 | * Unregister the callback that determines execution of transformers. 116 | */ 117 | public static function unguard(): void 118 | { 119 | static::$guardWith = null; 120 | } 121 | 122 | /**Get the guard callback.*/ 123 | public static function getGuardWith(): Closure 124 | { 125 | if (is_null(static::$guardWith)) { 126 | return fn () => true; 127 | } 128 | 129 | return static::$guardWith; 130 | } 131 | 132 | /** 133 | * Check if the transformer method should be delegated to the underlying object. 134 | */ 135 | protected function shouldDelegateTransformer($value, $method): bool 136 | { 137 | if (! is_string($method)) { 138 | return false; 139 | } 140 | 141 | return is_object($value) && str_starts_with(trim($method), '->'); 142 | } 143 | 144 | /** 145 | * Return a callback for passing closures/transformable classes 146 | * that throws an exception for aborting processing of tranformers. 147 | */ 148 | protected function abortTransformationCallback(): Closure 149 | { 150 | return function () { 151 | throw new AbortedTransformationException(); 152 | }; 153 | } 154 | 155 | /**Set the value to transform.*/ 156 | public function setValue(mixed $value): static 157 | { 158 | $this->value = $value; 159 | 160 | return $this; 161 | } 162 | 163 | /**Set the transformers to apply on the data.*/ 164 | public function setFunctions(array|string $functions): static 165 | { 166 | if (is_string($functions)) { 167 | $functions = explode('|', $functions); 168 | } 169 | 170 | $this->functions = $functions; 171 | 172 | return $this; 173 | } 174 | 175 | /** 176 | * Prepare the transformer function for execution. 177 | */ 178 | protected function prepareTransformerFunction($tranformer) 179 | { 180 | if (is_string($tranformer) && is_subclass_of($tranformer, Transformable::class)) { 181 | $tranformer = new $tranformer(); 182 | } 183 | 184 | return $tranformer; 185 | } 186 | 187 | /** 188 | * Prepare arguments for a function call on the value. 189 | */ 190 | protected function prepareArguments($value, $function, array $args = []) 191 | { 192 | // delegated function calls do not get the value passed in by default 193 | // since the set value is the transformer function itself, 194 | // the value parameter is not needed as a parameter to this function 195 | if ($this->shouldDelegateTransformer($value, $function)) { 196 | $defaults = []; 197 | } else { 198 | $defaults = [$value]; 199 | } 200 | 201 | $parameters = array_merge($defaults, $args); 202 | 203 | foreach ($parameters as $index => $param) { 204 | if (! is_string($param)) { 205 | continue; 206 | } 207 | 208 | if (trim($param) === ':value:') { 209 | $parameters[$index] = $value; 210 | array_shift($parameters); 211 | break; 212 | } 213 | // allow params to be to be casted to a specific type 214 | if (preg_match('/.+@(int|str|float|bool|array|object)/', $param, $matches)) { 215 | $type = $matches[1]; 216 | $param = rtrim($param, "@$type"); 217 | $parameters[$index] = match ($type) { 218 | 'int' => (int) $param, 219 | 'str' => (string) $param, 220 | 'float' => (float) $param, 221 | 'bool' => filter_var($param, FILTER_VALIDATE_BOOL), 222 | 'array' => (array) $param, 223 | 'object' => (object) $param, 224 | }; 225 | } 226 | 227 | } 228 | 229 | return $parameters; 230 | } 231 | 232 | /** 233 | * Apply the set transformer function on the value. 234 | */ 235 | public function transform() 236 | { 237 | foreach ($this->functions as $function) { 238 | [$rule, $parameters] = TransformerRuleParser::parse($function); 239 | 240 | // if rule is ? then check if the value is blank break out if it is. 241 | if ($rule == '?') { 242 | if (blank($this->value)) { 243 | break; 244 | } else { 245 | continue; 246 | } 247 | } 248 | 249 | try { 250 | $this->value = $this->call($rule, $this->value, $parameters); 251 | } catch (AbortedTransformationException) { 252 | break; 253 | } 254 | } 255 | 256 | return $this->value; 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/TransformerRuleParser.php: -------------------------------------------------------------------------------- 1 | validated() 17 | : $this->all()) 18 | : $input; 19 | 20 | return (new DataTransformer($input, $localTransformers)) 21 | ->transform(); 22 | }); 23 | } 24 | } --------------------------------------------------------------------------------