1<?php
2
3/**
4 * This file is part of the FreeDSx LDAP package.
5 *
6 * (c) Chad Sikorra <Chad.Sikorra@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace FreeDSx\Ldap\Entry;
13
14use ArrayIterator;
15use Countable;
16use IteratorAggregate;
17use Traversable;
18use function count;
19use function is_array;
20
21/**
22 * Represents an Entry in LDAP.
23 *
24 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
25 */
26class Entry implements IteratorAggregate, Countable
27{
28    /**
29     * @var Attribute[]
30     */
31    protected $attributes;
32
33    /**
34     * @var Dn
35     */
36    protected $dn;
37
38    /**
39     * @var Changes
40     */
41    protected $changes;
42
43    /**
44     * @param string|Dn $dn
45     * @param Attribute ...$attributes
46     */
47    public function __construct($dn, Attribute ...$attributes)
48    {
49        $this->dn = $dn instanceof Dn ? $dn : new Dn($dn);
50        $this->attributes = $attributes;
51        $this->changes = new Changes();
52    }
53
54    /**
55     * Add an attribute and its values.
56     *
57     * @param string|Attribute $attribute
58     * @param string ...$values
59     * @return $this
60     */
61    public function add($attribute, ...$values)
62    {
63        $attribute = $attribute instanceof Attribute ? $attribute : new Attribute($attribute, ...$values);
64
65        if (($exists = $this->get($attribute, true)) !== null) {
66            $exists->add(...$attribute->getValues());
67        } else {
68            $this->attributes[] = $attribute;
69        }
70        $this->changes->add(Change::add(clone $attribute));
71
72        return $this;
73    }
74
75    /**
76     * Remove an attribute's value(s).
77     *
78     * @param string|Attribute $attribute
79     * @param mixed|string ...$values
80     * @return $this
81     */
82    public function remove($attribute, ...$values)
83    {
84        $attribute = $attribute instanceof Attribute ? $attribute : new Attribute($attribute, ...$values);
85
86        if (count($attribute->getValues()) !== 0) {
87            if (($exists = $this->get($attribute, true)) !== null) {
88                $exists->remove(...$attribute->getValues());
89            }
90            $this->changes->add(Change::delete(clone $attribute));
91        }
92
93        return $this;
94    }
95
96    /**
97     * Reset an attribute, which removes any values it may have.
98     *
99     * @param string|Attribute ...$attributes
100     * @return $this
101     */
102    public function reset(...$attributes)
103    {
104        foreach ($attributes as $attribute) {
105            $attribute = $attribute instanceof Attribute ? $attribute : new Attribute($attribute);
106            foreach ($this->attributes as $i => $attr) {
107                if ($attr->equals($attribute, true)) {
108                    unset($this->attributes[$i]);
109                    break;
110                }
111            }
112            $this->changes()->add(Change::reset(clone $attribute));
113        }
114
115        return $this;
116    }
117
118    /**
119     * Set an attribute on the entry, replacing any value(s) that may exist on it.
120     *
121     * @param string|Attribute $attribute
122     * @param mixed ...$values
123     * @return $this
124     */
125    public function set($attribute, ...$values)
126    {
127        $attribute = $attribute instanceof Attribute ? $attribute : new Attribute($attribute, ...$values);
128
129        $exists = false;
130        foreach ($this->attributes as $i => $attr) {
131            if ($attr->equals($attribute, true)) {
132                $exists = true;
133                $this->attributes[$i] = $attribute;
134                break;
135            }
136        }
137        if (!$exists) {
138            $this->attributes[] = $attribute;
139        }
140        $this->changes->add(Change::replace(clone $attribute));
141
142        return $this;
143    }
144
145    /**
146     * Get a specific attribute by name (or Attribute object).
147     *
148     * @param string|Attribute $attribute
149     * @param bool $strict If set to true, then options on the attribute must also match.
150     * @return null|Attribute
151     */
152    public function get($attribute, bool $strict = false): ?Attribute
153    {
154        $attribute = $attribute instanceof Attribute ? $attribute : new Attribute($attribute);
155
156        foreach ($this->attributes as $attr) {
157            if ($attr->equals($attribute, $strict)) {
158                return $attr;
159            }
160        }
161
162        return null;
163    }
164
165    /**
166     * Check if a specific attribute exists on the entry.
167     *
168     * @param string|Attribute $attribute
169     * @param bool $strict
170     * @return bool
171     */
172    public function has($attribute, bool $strict = false): bool
173    {
174        $attribute = $attribute instanceof Attribute ? $attribute : new Attribute($attribute);
175
176        return (bool) $this->get($attribute, $strict);
177    }
178
179    /**
180     * @return Attribute[]
181     */
182    public function getAttributes(): array
183    {
184        return $this->attributes;
185    }
186
187    /**
188     * @return Dn
189     */
190    public function getDn(): Dn
191    {
192        return $this->dn;
193    }
194
195    /**
196     * Get the changes accumulated for this entry.
197     *
198     * @return Changes
199     */
200    public function changes(): Changes
201    {
202        return $this->changes;
203    }
204
205    /**
206     * Get the entry representation as an associative array.
207     *
208     * @return array
209     */
210    public function toArray(): array
211    {
212        $attributes = [];
213
214        foreach ($this->attributes as $attribute) {
215            $attributes[$attribute->getDescription()] = $attribute->getValues();
216        }
217
218        return $attributes;
219    }
220
221    /**
222     * @inheritDoc
223     * @psalm-return \ArrayIterator<array-key, Attribute>
224     */
225    public function getIterator(): Traversable
226    {
227        return new ArrayIterator($this->attributes);
228    }
229
230    /**
231     * @return int
232     * @psalm-return 0|positive-int
233     */
234    public function count(): int
235    {
236        return count($this->attributes);
237    }
238
239    public function __toString(): string
240    {
241        return $this->dn->toString();
242    }
243
244    public function __get(string $name): ?Attribute
245    {
246        return $this->get($name);
247    }
248
249    /**
250     * @param string[]|string $value
251     */
252    public function __set(string $name, $value): void
253    {
254        $this->set($name, ...(is_array($value) ? $value : [$value]));
255    }
256
257    public function __isset(string $name): bool
258    {
259        return $this->has($name);
260    }
261
262    public function __unset(string $name): void
263    {
264        $this->reset($name);
265    }
266
267    /**
268     * An alias of fromArray().
269     *
270     * @param string $dn
271     * @param array $attributes
272     * @return Entry
273     */
274    public static function create(string $dn, array $attributes = []): Entry
275    {
276        return self::fromArray($dn, $attributes);
277    }
278
279    /**
280     * Construct an entry from an associative array.
281     *
282     * @param string $dn
283     * @param array $attributes
284     * @return Entry
285     */
286    public static function fromArray(string $dn, array $attributes = []): Entry
287    {
288        /** @var Attribute[] $entryAttr */
289        $entryAttr = [];
290
291        foreach ($attributes as $attribute => $value) {
292            $entryAttr[] = new Attribute($attribute, ...(is_array($value) ? $value : [$value]));
293        }
294
295        return new self($dn, ...$entryAttr);
296    }
297}
298