Completed
Push — master ( 26275a...558df7 )
by Hong
02:43
created

ReferenceTrait::deReference()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 10
Bugs 1 Features 2
Metric Value
c 10
b 1
f 2
dl 0
loc 24
rs 8.9713
cc 3
eloc 11
nc 3
nop 1
1
<?php
2
/**
3
 * Phossa Project
4
 *
5
 * PHP version 5.4
6
 *
7
 * @category  Library
8
 * @package   Phossa2\Shared
9
 * @copyright Copyright (c) 2016 phossa.com
10
 * @license   http://mit-license.org/ MIT License
11
 * @link      http://www.phossa.com/
12
 */
13
/*# declare(strict_types=1); */
14
15
namespace Phossa2\Shared\Reference;
16
17
use Phossa2\Shared\Message\Message;
18
use Phossa2\Shared\Cache\LocalCacheTrait;
19
use Phossa2\Shared\Exception\RuntimeException;
20
21
/**
22
 * ReferenceTrait
23
 *
24
 * Provides reference & dereference methods
25
 *
26
 * @package Phossa2\Shared
27
 * @author  Hong Zhang <[email protected]>
28
 * @see     ReferenceInterface
29
 * @version 2.0.8
30
 * @since   2.0.4 added
31
 * @since   2.0.6 added reference cache support
32
 * @since   2.0.8 added delegator support, changed to LocaclCache
33
 */
34
trait ReferenceTrait
35
{
36
    use LocalCacheTrait;
37
38
    /**
39
     * refernece start chars
40
     *
41
     * @var    string
42
     * @access private
43
     */
44
    private $ref_start = '${';
45
46
    /**
47
     * reference ending chars
48
     *
49
     * @var    string
50
     * @access private
51
     */
52
    private $ref_end = '}';
53
54
    /**
55
     * cached pattern to match
56
     *
57
     * @var    string
58
     * @access private
59
     */
60
    private $ref_pattern = '~(\$\{((?:(?!\$\{|\}).)+?)\})~';
61
62
    /**
63
     * {@inheritDoc}
64
     */
65
    public function setReferencePattern(
66
        /*# string */ $start,
67
        /*# string */ $end
68
    ) {
69
        $this->ref_start = $start;
70
        $this->ref_end = $end;
71
72
        // build pattern on the fly
73
        $s = preg_quote($start);
74
        $e = preg_quote($end);
75
        $this->ref_pattern = sprintf(
76
            "~(%s((?:(?!%s|%s).)+?)%s)~", $s, $s, $e, $e
77
        );
78
79
        return $this->clearLocalCache();
80
    }
81
82
    /**
83
     * {@inheritDoc}
84
     */
85
    public function hasReference(
86
        /*# string */ $subject,
87
        array &$matched
88
    )/*# : bool */ {
89
        if (is_string($subject) &&
90
            false !== strpos($subject, $this->ref_start) &&
91
            preg_match($this->ref_pattern, $subject, $matched)
92
        ) {
93
            return true;
94
        }
95
        return false;
96
    }
97
98
    /**
99
     * {@inheritDoc}
100
     */
101
    public function deReference(/*# string */ $subject)
102
    {
103
        $loop = 0;
104
        $matched = [];
105
106
        // recursively dereference in the string $subject
107
        while ($this->hasReference($subject, $matched)) {
108
            // avoid looping
109
            $this->checkReferenceLoop($loop++, $matched[2]);
110
111
            // resolve the reference to a value
112
            $val = $this->resolveReference($matched[2]);
113
114
            // value is another string
115
            if (is_string($val)) {
116
                $subject = str_replace($matched[1], $val, $subject);
117
118
            // value is an array, object, null etc.
119
            } else {
120
                return $this->checkValue($val, $subject, $matched[1]);
121
            }
122
        }
123
        return $subject;
124
    }
125
126
    /**
127
     * {@inheritDoc}
128
     */
129
    public function deReferenceArray(&$dataArray)
130
    {
131
        if (is_string($dataArray)) {
132
            $dataArray = $this->deReference($dataArray);
133
        }
134
135
        if (!is_array($dataArray)) {
136
            return;
137
        }
138
139
        foreach ($dataArray as &$data) {
140
            $this->dereferenceArray($data);
141
        }
142
    }
143
144
    /**
145
     * Check dereferenced value
146
     *
147
     * @param  mixed $value
148
     * @param  string $subject the subject to dereference
149
     * @param  string $reference the matched whole reference
150
     * @return mixed
151
     * @throws RuntimeException if $subject malformed, like mix string & array
152
     * @access private
153
     */
154
    private function checkValue(
155
        $value,
156
        /*# string */ $subject,
157
        /*# string */ $reference
158
    ) {
159
        // unknown reference found, leave it alone
160
        if (is_null($value)) {
161
            // exception thrown in resolveUnknown() already if wanted to
162
            return $subject;
163
164
        // malformed partial match, partial string, partial non-scalar
165
        } elseif ($subject != $reference) {
166
            throw new RuntimeException(
167
                Message::get(Message::MSG_REF_MALFORMED, $reference),
168
                Message::MSG_REF_MALFORMED
169
            );
170
171
        // full match, array or object
172
        } else {
173
            return $value;
174
        }
175
    }
176
177
    /**
178
     * Resolve the reference $name
179
     *
180
     * @param  string $name
181
     * @return mixed
182
     * @throws RuntimeException if reference unknown
183
     * @access private
184
     * @since  2.0.6 added cache support
185
     * @since  2.0.8 added delegatorAware support
186
     */
187
    private function resolveReference(/*# string */ $name)
188
    {
189
        // try reference cache first
190
        if ($this->hasLocalCache($name)) {
191
            return $this->getLocalCache($name);
192
        }
193
194
        // lookup the reference
195
        $val = $this->referenceLookup($name);
196
197
        // dealing with unknown reference
198
        if (is_null($val)) {
199
            $val = $this->resolveUnknown($name);
200
        }
201
202
        // cache deref result
203
        $this->setLocalCache($name, $val);
204
205
        return $val;
206
    }
207
208
    /**
209
     * Throw exception if looped
210
     *
211
     * @param  int $loop loop counter
212
     * @param  string $name reference name
213
     * @throws RuntimeException if loop found
214
     * @access private
215
     * @since  2.0.6
216
     */
217
    private function checkReferenceLoop(
218
        /*# int */ $loop,
219
        /*# string */ $name
220
    ) {
221
        if ($loop > 20) {
222
            throw new RuntimeException(
223
                Message::get(Message::MSG_REF_LOOP, $name),
224
                Message::MSG_REF_LOOP
225
            );
226
        }
227
    }
228
229
    /**
230
     * Lookup reference with delegator or self
231
     *
232
     * @param  string $name
233
     * @return mixed
234
     * @access private
235
     */
236
    private function referenceLookup(/*# string */ $name)
237
    {
238
        // try delegator
239
        if ($this instanceof DelegatorAwareInterface &&
240
            $this->hasDelegator()
241
        ) {
242
            /* @var $delegator DelegatorInterface */
243
            $delegator = $this->getDelegator();
244
245
            if ($delegator->hasInLookup($name)) {
246
                $val = $delegator->getFromLookup($name);
247
            } else {
248
                $val = null;
249
            }
250
251
        // try self
252
        } else {
253
            $val = $this->getReference($name);
254
        }
255
256
        return $val;
257
    }
258
259
    /**
260
     * For unknown reference $name, normally returns NULL
261
     *
262
     * @param  string $name
263
     * @return mixed
264
     * @throws \Exception if implementor WANTS TO !!
265
     * @access protected
266
     */
267
    abstract protected function resolveUnknown(/*# string */ $name);
268
269
    /**
270
     * The REAL resolving method. return NULL for unknown reference
271
     *
272
     * @param  string $name
273
     * @return mixed
274
     * @access protected
275
     */
276
    abstract protected function getReference(/*# string */ $name);
277
}
278