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 FreeDSx\Ldap\Exception\InvalidArgumentException;
17use FreeDSx\Ldap\Exception\UnexpectedValueException;
18use IteratorAggregate;
19use Traversable;
20use function array_slice;
21use function count;
22use function implode;
23use function ltrim;
24use function preg_split;
25
26/**
27 * Represents a Distinguished Name.
28 *
29 * @author Chad Sikorra <Chad.Sikorra@gmail.com>
30 */
31class Dn implements IteratorAggregate, Countable
32{
33    /**
34     * @var string
35     */
36    protected $dn;
37
38    /**
39     * @var null|Rdn[]
40     */
41    protected $pieces;
42
43    /**
44     * @param string $dn
45     */
46    public function __construct(string $dn)
47    {
48        $this->dn = $dn;
49    }
50
51    /**
52     * @return Rdn
53     * @throws UnexpectedValueException
54     */
55    public function getRdn(): Rdn
56    {
57        if ($this->pieces === null) {
58            $this->parse();
59        }
60        if (!isset($this->pieces[0])) {
61            throw new UnexpectedValueException('The DN has no RDN.');
62        }
63
64        return $this->pieces[0];
65    }
66
67    /**
68     * @return null|Dn
69     * @throws UnexpectedValueException
70     */
71    public function getParent(): ?Dn
72    {
73        if ($this->pieces === null) {
74            $this->parse();
75        }
76        if (count((array) $this->pieces) < 2) {
77            return null;
78        }
79
80        return new Dn(implode(',', array_slice((array) $this->pieces, 1)));
81    }
82
83    /**
84     * @inheritDoc
85     * @@psalm-return \ArrayIterator<array-key, Rdn>
86     * @throws UnexpectedValueException
87     */
88    public function getIterator(): Traversable
89    {
90        return new ArrayIterator($this->toArray());
91    }
92
93    /**
94     * @return string
95     */
96    public function toString(): string
97    {
98        return $this->dn;
99    }
100
101    /**
102     * @inheritDoc
103     * @psalm-return 0|positive-int
104     * @throws UnexpectedValueException
105     */
106    public function count(): int
107    {
108        if ($this->pieces === null) {
109            $this->parse();
110        }
111
112        return count((array) $this->pieces);
113    }
114
115    /**
116     * @return string
117     */
118    public function __toString()
119    {
120        return $this->dn;
121    }
122
123    /**
124     * @return Rdn[]
125     * @throws UnexpectedValueException
126     */
127    public function toArray(): array
128    {
129        if ($this->pieces !== null) {
130            return $this->pieces;
131        }
132        $this->parse();
133
134        return ($this->pieces === null) ? [] : $this->pieces;
135    }
136
137    /**
138     * @param string $dn
139     * @return bool
140     */
141    public static function isValid(string $dn): bool
142    {
143        try {
144            (new self($dn))->toArray();
145
146            return true;
147        } catch (UnexpectedValueException | InvalidArgumentException $e) {
148            return false;
149        }
150    }
151
152    /**
153     * @todo This needs proper handling. But the regex would probably be rather crazy.
154     *
155     * @throws UnexpectedValueException
156     */
157    protected function parse(): void
158    {
159        if ($this->dn === '') {
160            $this->pieces = [];
161
162            return;
163        }
164        $pieces = preg_split('/(?<!\\\\),/', $this->dn);
165        $pieces = ($pieces === false) ? [] : $pieces;
166
167        if (count($pieces) === 0) {
168            throw new UnexpectedValueException(sprintf(
169                'The DN value "%s" is not valid.',
170                $this->dn
171            ));
172        }
173
174        $rdns = [];
175        foreach ($pieces as $i => $piece) {
176            $rdns[$i] = Rdn::create(ltrim($piece));
177        }
178
179        $this->pieces = $rdns;
180    }
181}
182