Issues (3)

src/Charcoal/Presenter/Presenter.php (3 issues)

1
<?php
2
3
namespace Charcoal\Presenter;
4
5
use ArrayAccess;
6
use Charcoal\Model\CollectionInterface;
0 ignored issues
show
The type Charcoal\Model\CollectionInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use InvalidArgumentException;
8
use Traversable;
9
10
/**
11
 * Presenter provides a presentation and transformation layer for a "model".
12
 *
13
 * It transforms (serializes) any data model (objects or array) into a presentation array, according to a **transformer**.
14
 *
15
 * A **transformer** defines the morph rules
16
 *
17
 * - A simple array or Traversable object, contain
18
 */
19
class Presenter
20
{
21
    /**
22
     * @var callable $transformer
23
     */
24
    private $transformer;
25
26
    /**
27
     * @var string $getterPattern
28
     */
29
    private $getterPattern;
30
31
    /**
32
     * @param array|Traversable|callable $transformer   The data-view transformation array (or Traversable) object.
33
     * @param string                     $getterPattern The string pattern to match string with. Must have a single catch-block.
34
     */
35
    public function __construct($transformer, $getterPattern = '~{{(\w*?)}}~')
36
    {
37
        $this->setTransformer($transformer);
38
        $this->getterPattern = $getterPattern;
39
    }
40
41
    /**
42
     * @param array|Traversable|callable $transformer The data-view transformation array (or Traversable) object.
43
     * @throws InvalidArgumentException If the provided transformer is not valid.
44
     * @return void
45
     */
46
    private function setTransformer($transformer)
47
    {
48
        if (is_callable($transformer)) {
49
            $this->transformer = $transformer;
0 ignored issues
show
Documentation Bug introduced by
It seems like $transformer can also be of type Traversable. However, the property $transformer is declared as type callable. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
50
        } elseif (is_array($transformer) || $transformer instanceof Traversable) {
51
            $this->transformer = function($model) use ($transformer) {
52
                return $transformer;
53
            };
54
        } else {
55
            throw new InvalidArgumentException(
56
                'Transformer must be an array or a Traversable object'
57
            );
58
        }
59
    }
60
61
    /**
62
     * TheT Transformer class is callable. Its purpose is to transform a model (object) into view data.
63
     *
64
     * The transformer is set from the constructor.
65
     *
66
     * @param mixed $obj The original data (object / model) to transform into view-data.
67
     * @return array Normalized data, suitable as presentation (view) layer
68
     */
69
    public function transform($obj)
70
    {
71
        $transformer = $this->transformer;
72
        return $this->transmogrify($obj, $transformer($obj));
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->transmogri...bj, $transformer($obj)) also could return the type boolean|string which is incompatible with the documented return type array.
Loading history...
73
    }
74
75
    /**
76
     * @param mixed $collection A collection or array to transform.
77
     * @return array
78
     */
79
    public function transformCollection($collection)
80
    {
81
        $array = [];
82
83
        if ($collection instanceof CollectionInterface) {
84
            $array = $collection->values();
85
        } elseif (is_array($collection)) {
86
            $array = $collection;
87
        }
88
89
        return array_map([$this, 'transform'], $array);
90
    }
91
92
    /**
93
     * Transmogrify an object into an other structure.
94
     *
95
     * @param mixed $obj Source object.
96
     * @param mixed $val Modifier.
97
     * @throws InvalidArgumentException If the modifier is not callable, traversable (array) or string.
98
     * @return mixed The transformed data (type depends on modifier).
99
     */
100
    private function transmogrify($obj, $val)
101
    {
102
        // Callbacks (lambda or callable) are supported. They must accept the source object as argument.
103
        if (!is_string($val) && is_callable($val)) {
104
            return $val($obj);
105
        }
106
107
        // Arrays or traversables are handled recursively.
108
        // This also converts / casts any Traversable into a simple array.
109
        if (is_array($val) || $val instanceof Traversable) {
110
            $data = [];
111
            foreach ($val as $k => $v) {
112
                if (!is_string($k)) {
113
                    if (is_string($v)) {
114
                        $data[$v] = $this->objectGet($obj, $v);
115
                    } else {
116
                        $data[] = $v;
117
                    }
118
                } else {
119
                    $data[$k] = $this->transmogrify($obj, $v);
120
                }
121
            }
122
            return $data;
123
        }
124
125
        // Strings are handled by rendering {{property}}  with dynamic object getter pattern.
126
        if (is_string($val)) {
127
            return preg_replace_callback($this->getterPattern, function(array $matches) use ($obj) {
128
                return $this->objectGet($obj, $matches[1]);
129
            }, $val);
130
        }
131
132
        if (is_numeric($val)) {
133
            return $val;
134
        }
135
136
        if (is_bool($val)) {
137
            return !!$val;
138
        }
139
140
        if ($val === null) {
141
            return null;
142
        }
143
144
        // Any other
145
        throw new InvalidArgumentException(
146
            sprintf(
147
                'Presenter\'s transmogrify val needs to be callable, traversable (array) or a string. "%s" given.',
148
                gettype($val)
149
            )
150
        );
151
    }
152
153
    /**
154
     * General-purpose dynamic object "getter".
155
     *
156
     * This method tries to fetch a "property" from any type of object (or array),
157
     * trying to figure out the best possible way:
158
     *
159
     * - Method call (`$obj->property()`)
160
     * - Public property get (`$obj->property`)
161
     * - Array access, if available (`$obj[property]`)
162
     * - Returns the property unchanged, otherwise
163
     *
164
     * @param mixed  $obj          The model (object or array) to retrieve the property's value from.
165
     * @param string $propertyName The property name (key) to retrieve from model.
166
     * @throws InvalidArgumentException If the property name is not a string.
167
     * @return mixed The object property, if available. The property name, unchanged, if it's not available.
168
     */
169
    private function objectGet($obj, $propertyName)
170
    {
171
        if (is_callable([$obj, $propertyName])) {
172
            return $obj->{$propertyName}();
173
        }
174
175
        if (isset($obj->{$propertyName})) {
176
            return $obj->{$propertyName};
177
        }
178
179
        if (is_string($propertyName) && (is_array($obj) || $obj instanceof ArrayAccess) && (isset($obj[$propertyName]))) {
180
            return $obj[$propertyName];
181
        }
182
183
        return null;
184
    }
185
}
186