1: <?php
2: namespace Peridot\Leo\Matcher;
3:
4: use Peridot\Leo\Matcher\Template\ArrayTemplate;
5: use Peridot\Leo\Matcher\Template\TemplateInterface;
6: use Peridot\Leo\ObjectPath\ObjectPath;
7: use Peridot\Leo\ObjectPath\ObjectPathValue;
8:
9: /**
10: * PropertyMatcher determines if the actual array or object has the expected property, and optionally matches
11: * an expected value for that property.
12: *
13: * @package Peridot\Leo\Matcher
14: */
15: class PropertyMatcher extends AbstractMatcher
16: {
17: /**
18: * @var string|int
19: */
20: protected $key;
21:
22: /**
23: * @var mixed
24: */
25: protected $value;
26:
27: /**
28: * @var mixed
29: */
30: protected $actualValue;
31:
32: /**
33: * @var bool
34: */
35: protected $actualValueSet = false;
36:
37: /**
38: * @var bool
39: */
40: protected $isDeep = false;
41:
42: /**
43: * @param mixed $key
44: * @param string $value
45: */
46: public function __construct($key, $value = "")
47: {
48: $this
49: ->setKey($key)
50: ->setValue($value);
51: }
52:
53: /**
54: * Return the expected object or array key.
55: *
56: * @return int|string
57: */
58: public function getKey()
59: {
60: return $this->key;
61: }
62:
63: /**
64: * Set the expected object or array key.
65: *
66: * @param int|string $key
67: * @return $this
68: */
69: public function setKey($key)
70: {
71: $this->key = $key;
72: return $this;
73: }
74:
75: /**
76: * Return the expected property value.
77: *
78: * @return mixed
79: */
80: public function getValue()
81: {
82: return $this->value;
83: }
84:
85: /**
86: * Set the expected property value.
87: *
88: * @param mixed $value
89: * @return $this
90: */
91: public function setValue($value)
92: {
93: $this->value = $value;
94: return $this;
95: }
96:
97: /**
98: * {@inheritdoc}
99: *
100: * @return TemplateInterface
101: */
102: public function getDefaultTemplate()
103: {
104: list($default, $negated) = $this->getTemplateStrings();
105:
106: $template = new ArrayTemplate([
107: 'default' => $default,
108: 'negated' => $negated
109: ]);
110:
111: return $template->setTemplateVars([
112: 'key' => $this->getKey(),
113: 'value' => $this->getValue(),
114: 'actualValue' => $this->getActualValue()
115: ]);
116: }
117:
118: /**
119: * Return the actual value given to the matcher.
120: *
121: * @return mixed
122: */
123: public function getActualValue()
124: {
125: return $this->actualValue;
126: }
127:
128: /**
129: * Set the actual value given to the matcher. Used to
130: * store whether or not the actual value was set.
131: *
132: * @param mixed $actualValue
133: * @return $this
134: */
135: public function setActualValue($actualValue)
136: {
137: $this->actualValue = $actualValue;
138: $this->actualValueSet = true;
139: return $this;
140: }
141:
142: /**
143: * Return if the actual value has been set.
144: *
145: * @return bool
146: */
147: public function isActualValueSet()
148: {
149: return $this->actualValueSet;
150: }
151:
152: /**
153: * Tell the property matcher to match deep properties.
154: *
155: * return $this
156: */
157: public function setIsDeep($isDeep)
158: {
159: $this->isDeep = $isDeep;
160: return $this;
161: }
162:
163: /**
164: * Return whether or not the matcher is matching deep properties.
165: *
166: * @return bool
167: */
168: public function isDeep()
169: {
170: return $this->isDeep;
171: }
172:
173: /**
174: * Matches if the actual value has a property, optionally matching
175: * the expected value of that property. If the deep flag is set,
176: * the matcher will use the ObjectPath utility to parse deep expressions.
177: *
178: * @code
179: *
180: * $this->doMatch('child->name->first', 'brian');
181: *
182: * @endcode
183: *
184: * @param mixed $actual
185: * @return mixed
186: */
187: protected function doMatch($actual)
188: {
189: $this->validateActual($actual);
190:
191: if ($this->isDeep()) {
192: return $this->matchDeep($actual);
193: }
194:
195: $actual = $this->actualToArray($actual);
196:
197: return $this->matchArrayIndex($actual);
198: }
199:
200: /**
201: * Convert the actual value to an array, whether it is an object or an array.
202: *
203: * @param object|array $actual
204: * @return array|object
205: */
206: protected function actualToArray($actual)
207: {
208: if (is_object($actual)) {
209: return get_object_vars($actual);
210: }
211: return $actual;
212: }
213:
214: /**
215: * Match that an array index exists, and matches
216: * the expected value if set.
217: *
218: * @param $actual
219: * @return bool
220: */
221: protected function matchArrayIndex($actual)
222: {
223: if (isset($actual[$this->getKey()])) {
224: $this->assertion->setActual($actual[$this->getKey()]);
225: return $this->isExpected($actual[$this->getKey()]);
226: }
227:
228: return false;
229: }
230:
231: /**
232: * Uses ObjectPath to parse an expression if the deep flag
233: * is set.
234: *
235: * @param $actual
236: * @return bool
237: */
238: protected function matchDeep($actual)
239: {
240: $path = new ObjectPath($actual);
241: $value = $path->get($this->getKey());
242:
243: if (is_null($value)) {
244: return false;
245: }
246:
247: $this->assertion->setActual($value->getPropertyValue());
248:
249: return $this->isExpected($value->getPropertyValue());
250: }
251:
252: /**
253: * Check if the given value is expected.
254: *
255: * @param $value
256: * @return bool
257: */
258: protected function isExpected($value)
259: {
260: if ($expected = $this->getValue()) {
261: $this->setActualValue($value);
262: return $this->getActualValue() === $expected;
263: }
264:
265: return true;
266: }
267:
268: /**
269: * Ensure that the actual value is an object or an array.
270: *
271: * @param $actual
272: */
273: protected function validateActual($actual)
274: {
275: if (!is_object($actual) && !is_array($actual)) {
276: throw new \InvalidArgumentException("PropertyMatcher expects an object or an array");
277: }
278: }
279:
280: /**
281: * Returns the strings used in creating the template for the matcher.
282: *
283: * @return array
284: */
285: protected function getTemplateStrings()
286: {
287: $default = "Expected {{actual}} to have a{{deep}}property {{key}}";
288: $negated = "Expected {{actual}} to not have a{{deep}}property {{key}}";
289:
290: if ($this->getValue() && $this->isActualValueSet()) {
291: $default = "Expected {{actual}} to have a{{deep}}property {{key}} of {{value}}, but got {{actualValue}}";
292: $negated = "Expected {{actual}} to not have a{{deep}}property {{key}} of {{value}}";
293: }
294:
295: $deep = ' ';
296: if ($this->isDeep()) {
297: $deep = ' deep ';
298: }
299:
300: return str_replace('{{deep}}', $deep, [$default, $negated]);
301: }
302: }
303: