Completed
Push — master ( 3a00e0...6c20a7 )
by Ben
01:10
created

HasMagicAttributes::attr()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
c 0
b 0
f 0
rs 10
cc 2
nc 2
nop 3
1
<?php
2
3
namespace Thinktomorrow\MagicAttributes;
4
5
trait HasMagicAttributes
6
{
7
    /**
8
     * Flag that allows to allow or disallow public retrieval of the magic attributes
9
     * via the provided 'attr()' method. Set this to true in case you'd like to
10
     * control the attribute retrieval yourself. Any attempt to use
11
     * the 'attr' method will throw an exception.
12
     *
13
     * @var bool
14
     */
15
    public $disallow_magic_api = false;
16
17
    public function attr($key, $default = null, $closure = null)
18
    {
19
        if($this->disallow_magic_api){
20
            throw new DisallowedMagicAttributeUsage('Attempt to fetch magic value for ['.$key.'], but magic attribute retrieval is set to prohibited.');
21
        }
22
23
        return $this->magicAttribute($key, $default, $closure);
24
    }
25
26
    protected function magicAttribute($key, $default = null, $closure = null)
27
    {
28
        if (method_exists($this, $key)) {
29
            return $this->{$key}();
30
        }
31
32
        /**
33
         * We first try to fetch the key as is, and then we try to fetch
34
         * with converting camelcase to dot syntax as well.
35
         */
36
        $value = null;
37
38
        foreach ([$key, $this->camelCaseToDotSyntax($key)] as $k) {
39
            if (null !== ($value = $this->retrieveAttributeValue($k))) {
40
                break;
41
            }
42
        }
43
44
        if (is_null($value)) {
45
            return $default;
46
        }
47
48
        return is_callable($closure)
49
            ? call_user_func_array($closure, [$value, $this])
50
            : $value;
51
    }
52
53
    private function retrieveAttributeValue($key)
54
    {
55
        $keys = explode('.', $key);
56
        $parent = $this;
57
        $value = null;
58
59
        foreach ($keys as $k) {
60
            $value = $this->retrieveValue($k, $parent);
61
62
            if (is_null($value)) {
63
                return null;
64
            }
65
66
            $parent = $value;
67
        }
68
69
        return $value;
70
    }
71
72
    private function retrieveValue($key, $parent)
73
    {
74
        if (null !== ($value = $this->retrieveProperty($key, $parent))) {
75
            return $value;
76
        }
77
78
        /**
79
         * At this point, we know that the key isn't present as property.
80
         * We now check if its an array consisting itself of nested items
81
         * so we can try to pluck the values by key from those arrays / objects.
82
         */
83
        if (! $this->isMultiDimensional($parent)) {
84
            return null;
85
        }
86
87
        return $this->pluck($key, $parent);
88
    }
89
90
    private function isMultiDimensional($array): bool
91
    {
92
        // A eloquent collection is always considered multidimensional
93
        if ($this->isCollection($array)) {
94
            return true;
95
        }
96
97
        if (!is_array($array)) {
98
            return false;
99
        }
100
101
        if (count($array) != count($array, COUNT_RECURSIVE)) {
102
            return true;
103
        }
104
105
        // If count is the same, it still could be a list of objects
106
        // which we will treat the same as a multidim. array
107
        return is_object(reset($array));
108
    }
109
110
    private function pluck($key, $list)
111
    {
112
        if (! $this->isAccessibleAsArray($list)) {
113
            return null;
114
        }
115
116
        $values = [];
117
118
        foreach ($list as $item) {
119
            if ($value = $this->retrieveProperty($key, $item)) {
120
                $values[] = $value;
121
            }
122
        }
123
124
        return count($values) > 0 ? $values : null;
125
    }
126
127
    private function retrieveProperty($key, $parent)
128
    {
129
        if (is_object($parent) && isset($parent->$key)) {
130
            return $parent->$key;
131
        }
132
133
        if ($this->isAccessibleAsArray($parent) && isset($parent[$key])) {
134
            return $parent[$key];
135
        }
136
137
        return null;
138
    }
139
140
    /**
141
     * @param $key
142
     * @return string
143
     */
144
    private function camelCaseToDotSyntax($key): string
145
    {
146
        return strtolower(preg_replace('/(?<!^)[A-Z]/', '.$0', $key));
147
    }
148
149
    private function isAccessibleAsArray($value)
150
    {
151
        return is_array($value) || $this->isCollection($value);
152
    }
153
154
    /**
155
     * Check if value is a Collection with ArrayAccess.
156
     *
157
     * @param $value
158
     * @return bool
159
     */
160
    private function isCollection($value)
161
    {
162
        return (is_object($value) &&  $value instanceof \ArrayAccess);
163
    }
164
}
165