first commit

This commit is contained in:
2025-07-18 16:20:14 +07:00
commit 98af45c018
16382 changed files with 3148096 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\AdapterInterface;
use Symfony\Component\Ldap\Adapter\ConnectionInterface;
use Symfony\Component\Ldap\Adapter\EntryManagerInterface;
use Symfony\Component\Ldap\Adapter\QueryInterface;
use Symfony\Component\Ldap\Exception\LdapException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Adapter implements AdapterInterface
{
private array $config;
private ConnectionInterface $connection;
private EntryManagerInterface $entryManager;
public function __construct(array $config = [])
{
if (!\extension_loaded('ldap')) {
throw new LdapException('The LDAP PHP extension is not enabled.');
}
$this->config = $config;
}
public function getConnection(): ConnectionInterface
{
return $this->connection ??= new Connection($this->config);
}
public function getEntryManager(): EntryManagerInterface
{
return $this->entryManager ??= new EntryManager($this->getConnection());
}
public function createQuery(string $dn, string $query, array $options = []): QueryInterface
{
return new Query($this->getConnection(), $dn, $query, $options);
}
public function escape(string $subject, string $ignore = '', int $flags = 0): string
{
$value = ldap_escape($subject, $ignore, $flags);
// Per RFC 4514, leading/trailing spaces should be encoded in DNs, as well as carriage returns.
if ($flags & \LDAP_ESCAPE_DN) {
if (!empty($value) && ' ' === $value[0]) {
$value = '\\20'.substr($value, 1);
}
if (!empty($value) && ' ' === $value[\strlen($value) - 1]) {
$value = substr($value, 0, -1).'\\20';
}
$value = str_replace("\r", '\0d', $value);
}
return $value;
}
}

View File

@@ -0,0 +1,137 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\CollectionInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\LdapException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Collection implements CollectionInterface
{
private Connection $connection;
private Query $search;
/** @var list<Entry> */
private array $entries;
public function __construct(Connection $connection, Query $search)
{
$this->connection = $connection;
$this->search = $search;
}
public function toArray(): array
{
return $this->entries ??= iterator_to_array($this->getIterator(), false);
}
public function count(): int
{
$con = $this->connection->getResource();
$searches = $this->search->getResources();
$count = 0;
foreach ($searches as $search) {
$searchCount = ldap_count_entries($con, $search);
if (false === $searchCount) {
throw new LdapException('Error while retrieving entry count: '.ldap_error($con));
}
$count += $searchCount;
}
return $count;
}
public function getIterator(): \Traversable
{
if (0 === $this->count()) {
return;
}
$con = $this->connection->getResource();
$searches = $this->search->getResources();
foreach ($searches as $search) {
$current = ldap_first_entry($con, $search);
if (false === $current) {
throw new LdapException('Could not rewind entries array: '.ldap_error($con));
}
yield $this->getSingleEntry($con, $current);
while (false !== $current = ldap_next_entry($con, $current)) {
yield $this->getSingleEntry($con, $current);
}
}
}
public function offsetExists(mixed $offset): bool
{
$this->toArray();
return isset($this->entries[$offset]);
}
public function offsetGet(mixed $offset): ?Entry
{
$this->toArray();
return $this->entries[$offset] ?? null;
}
public function offsetSet(mixed $offset, mixed $value): void
{
$this->toArray();
$this->entries[$offset] = $value;
}
public function offsetUnset($offset): void
{
$this->toArray();
unset($this->entries[$offset]);
}
private function getSingleEntry($con, $current): Entry
{
$attributes = ldap_get_attributes($con, $current);
if (false === $attributes) {
throw new LdapException('Could not fetch attributes: '.ldap_error($con));
}
$attributes = $this->cleanupAttributes($attributes);
$dn = ldap_get_dn($con, $current);
if (false === $dn) {
throw new LdapException('Could not fetch DN: '.ldap_error($con));
}
return new Entry($dn, $attributes);
}
private function cleanupAttributes(array $entry): array
{
$attributes = array_diff_key($entry, array_flip(range(0, $entry['count'] - 1)) + [
'count' => null,
'dn' => null,
]);
array_walk($attributes, function (&$value) {
unset($value['count']);
});
return $attributes;
}
}

View File

@@ -0,0 +1,189 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use LDAP\Connection as LDAPConnection;
use Symfony\Component\Ldap\Adapter\AbstractConnection;
use Symfony\Component\Ldap\Exception\AlreadyExistsException;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\Exception\ConnectionTimeoutException;
use Symfony\Component\Ldap\Exception\InvalidCredentialsException;
use Symfony\Component\Ldap\Exception\LdapException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Connection extends AbstractConnection
{
private const LDAP_INVALID_CREDENTIALS = 0x31;
private const LDAP_TIMEOUT = 0x55;
private const LDAP_ALREADY_EXISTS = 0x44;
private const PRECONNECT_OPTIONS = [
ConnectionOptions::DEBUG_LEVEL,
ConnectionOptions::X_TLS_CACERTDIR,
ConnectionOptions::X_TLS_CACERTFILE,
ConnectionOptions::X_TLS_REQUIRE_CERT,
];
private bool $bound = false;
private ?LDAPConnection $connection = null;
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
/**
* @return void
*/
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->disconnect();
}
public function isBound(): bool
{
return $this->bound;
}
/**
* @param string $password WARNING: When the LDAP server allows unauthenticated binds, a blank $password will always be valid
*
* @return void
*/
public function bind(?string $dn = null, #[\SensitiveParameter] ?string $password = null)
{
if (!$this->connection) {
$this->connect();
}
if (false === @ldap_bind($this->connection, $dn, $password)) {
$error = ldap_error($this->connection);
switch (ldap_errno($this->connection)) {
case self::LDAP_INVALID_CREDENTIALS:
throw new InvalidCredentialsException($error);
case self::LDAP_TIMEOUT:
throw new ConnectionTimeoutException($error);
case self::LDAP_ALREADY_EXISTS:
throw new AlreadyExistsException($error);
}
throw new ConnectionException($error);
}
$this->bound = true;
}
/**
* @internal
*/
public function getResource(): ?LDAPConnection
{
return $this->connection;
}
/**
* @return void
*/
public function setOption(string $name, array|string|int|bool $value)
{
if (!@ldap_set_option($this->connection, ConnectionOptions::getOption($name), $value)) {
throw new LdapException(sprintf('Could not set value "%s" for option "%s".', $value, $name));
}
}
/**
* @return array|string|int|null
*/
public function getOption(string $name)
{
if (!@ldap_get_option($this->connection, ConnectionOptions::getOption($name), $ret)) {
throw new LdapException(sprintf('Could not retrieve value for option "%s".', $name));
}
return $ret;
}
/**
* @return void
*/
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefault('debug', false);
$resolver->setAllowedTypes('debug', 'bool');
$resolver->setDefault('referrals', false);
$resolver->setAllowedTypes('referrals', 'bool');
$resolver->setDefault('options', function (OptionsResolver $options, Options $parent) {
$options->setDefined(array_map('strtolower', array_keys((new \ReflectionClass(ConnectionOptions::class))->getConstants())));
if (true === $parent['debug']) {
$options->setDefault('debug_level', 7);
}
if (!isset($parent['network_timeout'])) {
$options->setDefault('network_timeout', \ini_get('default_socket_timeout'));
}
$options->setDefaults([
'protocol_version' => $parent['version'],
'referrals' => $parent['referrals'],
]);
});
}
private function connect(): void
{
if ($this->connection) {
return;
}
foreach ($this->config['options'] as $name => $value) {
if (\in_array(ConnectionOptions::getOption($name), self::PRECONNECT_OPTIONS, true)) {
$this->setOption($name, $value);
}
}
if (false === $connection = ldap_connect($this->config['connection_string'])) {
throw new LdapException('Invalid connection string: '.$this->config['connection_string']);
} else {
$this->connection = $connection;
}
foreach ($this->config['options'] as $name => $value) {
if (!\in_array(ConnectionOptions::getOption($name), self::PRECONNECT_OPTIONS, true)) {
$this->setOption($name, $value);
}
}
if ('tls' === $this->config['encryption'] && false === @ldap_start_tls($this->connection)) {
throw new LdapException('Could not initiate TLS connection: '.ldap_error($this->connection));
}
}
private function disconnect(): void
{
if ($this->connection) {
ldap_unbind($this->connection);
}
$this->connection = null;
$this->bound = false;
}
}

View File

@@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Exception\LdapException;
/**
* A class representing the Ldap extension's options, which can be used with
* ldap_set_option or ldap_get_option.
*
* @author Charles Sarrazin <charles@sarraz.in>
*
* @internal
*/
final class ConnectionOptions
{
public const API_INFO = 0x00;
public const DEREF = 0x02;
public const SIZELIMIT = 0x03;
public const TIMELIMIT = 0x04;
public const REFERRALS = 0x08;
public const RESTART = 0x09;
public const PROTOCOL_VERSION = 0x11;
public const SERVER_CONTROLS = 0x12;
public const CLIENT_CONTROLS = 0x13;
public const API_FEATURE_INFO = 0x15;
public const HOST_NAME = 0x30;
public const ERROR_NUMBER = 0x31;
public const ERROR_STRING = 0x32;
public const MATCHED_DN = 0x33;
public const DEBUG_LEVEL = 0x5001;
public const TIMEOUT = 0x5002;
public const NETWORK_TIMEOUT = 0x5005;
public const X_TLS_CACERTFILE = 0x6002;
public const X_TLS_CACERTDIR = 0x6003;
public const X_TLS_CERTFILE = 0x6004;
public const X_TLS_CRL_ALL = 0x02;
public const X_TLS_CRL_NONE = 0x00;
public const X_TLS_CRL_PEER = 0x01;
public const X_TLS_KEYFILE = 0x6005;
public const X_TLS_REQUIRE_CERT = 0x6006;
public const X_TLS_PROTOCOL_MIN = 0x6007;
public const X_TLS_CIPHER_SUITE = 0x6008;
public const X_TLS_RANDOM_FILE = 0x6009;
public const X_TLS_CRLFILE = 0x6010;
public const X_TLS_PACKAGE = 0x6011;
public const X_TLS_CRLCHECK = 0x600B;
public const X_TLS_DHFILE = 0x600E;
public const X_SASL_MECH = 0x6100;
public const X_SASL_REALM = 0x6101;
public const X_SASL_AUTHCID = 0x6102;
public const X_SASL_AUTHZID = 0x6103;
public const X_KEEPALIVE_IDLE = 0x6300;
public const X_KEEPALIVE_PROBES = 0x6301;
public const X_KEEPALIVE_INTERVAL = 0x6302;
public static function getOptionName(string $name): string
{
return sprintf('%s::%s', self::class, strtoupper($name));
}
/**
* Fetches an option's corresponding constant value from an option name.
* The option name can either be in snake or camel case.
*
* @throws LdapException
*/
public static function getOption(string $name): int
{
// Convert
$constantName = self::getOptionName($name);
if (!\defined($constantName)) {
throw new LdapException(sprintf('Unknown option "%s".', $name));
}
return \constant($constantName);
}
public static function isOption(string $name): bool
{
return \defined(self::getOptionName($name));
}
}

View File

@@ -0,0 +1,189 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use LDAP\Connection as LDAPConnection;
use Symfony\Component\Ldap\Adapter\EntryManagerInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\LdapException;
use Symfony\Component\Ldap\Exception\NotBoundException;
use Symfony\Component\Ldap\Exception\UpdateOperationException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
* @author Bob van de Vijver <bobvandevijver@hotmail.com>
*/
class EntryManager implements EntryManagerInterface
{
public function __construct(
private Connection $connection,
) {
}
/**
* @return $this
*/
public function add(Entry $entry)
{
$con = $this->getConnectionResource();
if (!@ldap_add($con, $entry->getDn(), $entry->getAttributes())) {
throw new LdapException(sprintf('Could not add entry "%s": ', $entry->getDn()).ldap_error($con), ldap_errno($con));
}
return $this;
}
/**
* @return $this
*/
public function update(Entry $entry)
{
$con = $this->getConnectionResource();
if (!@ldap_modify($con, $entry->getDn(), $entry->getAttributes())) {
throw new LdapException(sprintf('Could not update entry "%s": ', $entry->getDn()).ldap_error($con), ldap_errno($con));
}
return $this;
}
/**
* @return $this
*/
public function remove(Entry $entry)
{
$con = $this->getConnectionResource();
if (!@ldap_delete($con, $entry->getDn())) {
throw new LdapException(sprintf('Could not remove entry "%s": ', $entry->getDn()).ldap_error($con), ldap_errno($con));
}
return $this;
}
/**
* Adds values to an entry's multi-valued attribute from the LDAP server.
*
* @return $this
*
* @throws NotBoundException
* @throws LdapException
*/
public function addAttributeValues(Entry $entry, string $attribute, array $values)
{
$con = $this->getConnectionResource();
if (!@ldap_mod_add($con, $entry->getDn(), [$attribute => $values])) {
throw new LdapException(sprintf('Could not add values to entry "%s", attribute "%s": ', $entry->getDn(), $attribute).ldap_error($con), ldap_errno($con));
}
return $this;
}
/**
* Removes values from an entry's multi-valued attribute from the LDAP server.
*
* @return $this
*
* @throws NotBoundException
* @throws LdapException
*/
public function removeAttributeValues(Entry $entry, string $attribute, array $values)
{
$con = $this->getConnectionResource();
if (!@ldap_mod_del($con, $entry->getDn(), [$attribute => $values])) {
throw new LdapException(sprintf('Could not remove values from entry "%s", attribute "%s": ', $entry->getDn(), $attribute).ldap_error($con), ldap_errno($con));
}
return $this;
}
/**
* @return $this
*/
public function rename(Entry $entry, string $newRdn, bool $removeOldRdn = true)
{
$con = $this->getConnectionResource();
if (!@ldap_rename($con, $entry->getDn(), $newRdn, '', $removeOldRdn)) {
throw new LdapException(sprintf('Could not rename entry "%s" to "%s": ', $entry->getDn(), $newRdn).ldap_error($con), ldap_errno($con));
}
return $this;
}
/**
* Moves an entry on the Ldap server.
*
* @return $this
*
* @throws NotBoundException if the connection has not been previously bound
* @throws LdapException if an error is thrown during the rename operation
*/
public function move(Entry $entry, string $newParent)
{
$rdn = $this->parseRdnFromEntry($entry);
$con = $this->getConnectionResource();
// deleteOldRdn does not matter here, since the Rdn will not be changing in the move.
if (!@ldap_rename($con, $entry->getDn(), $rdn, $newParent, true)) {
throw new LdapException(sprintf('Could not move entry "%s" to "%s": ', $entry->getDn(), $newParent).ldap_error($con), ldap_errno($con));
}
return $this;
}
/**
* Get the connection resource, but first check if the connection is bound.
*/
private function getConnectionResource(): LDAPConnection
{
// If the connection is not bound, throw an exception. Users should use an explicit bind call first.
if (!$this->connection->isBound()) {
throw new NotBoundException('Query execution is not possible without binding the connection first.');
}
return $this->connection->getResource();
}
/**
* @param iterable<int, UpdateOperation> $operations An array or iterable of UpdateOperation instances
*
* @return $this
*
* @throws UpdateOperationException in case of an error
*/
public function applyOperations(string $dn, iterable $operations)
{
$operationsMapped = [];
foreach ($operations as $modification) {
$operationsMapped[] = $modification->toArray();
}
$con = $this->getConnectionResource();
if (!@ldap_modify_batch($con, $dn, $operationsMapped)) {
throw new UpdateOperationException(sprintf('Error executing UpdateOperation on "%s": ', $dn).ldap_error($con), ldap_errno($con));
}
return $this;
}
private function parseRdnFromEntry(Entry $entry): string
{
if (!preg_match('/(^[^,\\\\]*(?:\\\\.[^,\\\\]*)*),/', $entry->getDn(), $matches)) {
throw new LdapException(sprintf('Entry "%s" malformed, could not parse RDN.', $entry->getDn()));
}
return $matches[1];
}
}

View File

@@ -0,0 +1,209 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use LDAP\Result;
use Symfony\Component\Ldap\Adapter\AbstractQuery;
use Symfony\Component\Ldap\Adapter\CollectionInterface;
use Symfony\Component\Ldap\Exception\LdapException;
use Symfony\Component\Ldap\Exception\NotBoundException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
* @author Bob van de Vijver <bobvandevijver@hotmail.com>
*/
class Query extends AbstractQuery
{
public const PAGINATION_OID = \LDAP_CONTROL_PAGEDRESULTS;
/** @var Result[] */
private array $results;
private array $serverctrls = [];
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
/**
* @return void
*/
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$con = $this->connection->getResource();
if (!isset($this->results)) {
return;
}
foreach ($this->results as $result) {
if (false === $result || null === $result) {
continue;
}
if (!ldap_free_result($result)) {
throw new LdapException('Could not free results: '.ldap_error($con));
}
}
}
public function execute(): CollectionInterface
{
if (!isset($this->results)) {
// If the connection is not bound, throw an exception. Users should use an explicit bind call first.
if (!$this->connection->isBound()) {
throw new NotBoundException('Query execution is not possible without binding the connection first.');
}
$this->results = [];
$con = $this->connection->getResource();
$func = match ($this->options['scope']) {
static::SCOPE_BASE => 'ldap_read',
static::SCOPE_ONE => 'ldap_list',
static::SCOPE_SUB => 'ldap_search',
default => throw new LdapException(sprintf('Could not search in scope "%s".', $this->options['scope'])),
};
$itemsLeft = $maxItems = $this->options['maxItems'];
$pageSize = $this->options['pageSize'];
// Deal with the logic to handle maxItems properly. If we can satisfy it in
// one request based on pageSize, we don't need to bother sending page control
// to the server so that it can determine what we already know.
if (0 !== $maxItems && $pageSize > $maxItems) {
$pageSize = 0;
} elseif (0 !== $maxItems) {
$pageSize = min($maxItems, $pageSize);
}
$pageControl = $this->options['scope'] != static::SCOPE_BASE && $pageSize > 0;
$cookie = '';
do {
if ($pageControl) {
$this->controlPagedResult($pageSize, true, $cookie);
}
$sizeLimit = $itemsLeft;
if ($pageSize > 0 && $sizeLimit >= $pageSize) {
$sizeLimit = 0;
}
$search = @$func($con, $this->dn, $this->query, $this->options['filter'], $this->options['attrsOnly'], $sizeLimit, $this->options['timeout'], $this->options['deref'], $this->serverctrls);
if (false === $search) {
$ldapError = '';
if ($errno = ldap_errno($con)) {
$ldapError = sprintf(' LDAP error was [%d] %s', $errno, ldap_error($con));
}
if ($pageControl) {
$this->resetPagination();
}
throw new LdapException(sprintf('Could not complete search with dn "%s", query "%s" and filters "%s".%s.', $this->dn, $this->query, implode(',', $this->options['filter']), $ldapError), $errno);
}
$this->results[] = $search;
$itemsLeft -= min($itemsLeft, $pageSize);
if (0 !== $maxItems && 0 === $itemsLeft) {
break;
}
if ($pageControl) {
ldap_parse_result($con, $search, $errcode, $matcheddn, $errmsg, $referrals, $controls);
$cookie = $controls[\LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'] ?? '';
}
} while (null !== $cookie && '' !== $cookie);
if ($pageControl) {
$this->resetPagination();
}
}
return new Collection($this->connection, $this);
}
/**
* Returns an LDAP search resource. If this query resulted in multiple searches, only the first
* page will be returned.
*
* @internal
*/
public function getResource(int $idx = 0): ?Result
{
return $this->results[$idx] ?? null;
}
/**
* Returns all LDAP search resources.
*
* @return Result[]
*
* @internal
*/
public function getResources(): array
{
return $this->results;
}
/**
* Resets pagination on the current connection.
*/
private function resetPagination(): void
{
$con = $this->connection->getResource();
$this->controlPagedResult(0, false, '');
$this->serverctrls = [];
// This is a workaround for a bit of a bug in the above invocation
// of ldap_control_paged_result. Instead of indicating to extldap that
// we no longer wish to page queries on this link, this invocation sets
// the LDAP_CONTROL_PAGEDRESULTS OID with a page size of 0. This isn't
// well defined by RFC 2696 if there is no cookie present, so some servers
// will interpret it differently and do the wrong thing. Forcefully remove
// the OID for now until a fix can make its way through the versions of PHP
// the we support.
//
// This is not supported in PHP < 7.2, so these versions will remain broken.
$ctl = [];
ldap_get_option($con, \LDAP_OPT_SERVER_CONTROLS, $ctl);
if (!empty($ctl)) {
foreach ($ctl as $idx => $info) {
if (static::PAGINATION_OID == $info['oid']) {
unset($ctl[$idx]);
}
}
ldap_set_option($con, \LDAP_OPT_SERVER_CONTROLS, $ctl);
}
}
/**
* Sets LDAP pagination controls.
*/
private function controlPagedResult(int $pageSize, bool $critical, string $cookie): bool
{
$this->serverctrls = [
[
'oid' => \LDAP_CONTROL_PAGEDRESULTS,
'isCritical' => $critical,
'value' => [
'size' => $pageSize,
'cookie' => $cookie,
],
],
];
return true;
}
}

View File

@@ -0,0 +1,62 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Exception\UpdateOperationException;
class UpdateOperation
{
private int $operationType;
private ?array $values;
private string $attribute;
private const VALID_OPERATION_TYPES = [
\LDAP_MODIFY_BATCH_ADD,
\LDAP_MODIFY_BATCH_REMOVE,
\LDAP_MODIFY_BATCH_REMOVE_ALL,
\LDAP_MODIFY_BATCH_REPLACE,
];
/**
* @param int $operationType An LDAP_MODIFY_BATCH_* constant
* @param string $attribute The attribute to batch modify on
*
* @throws UpdateOperationException on consistency errors during construction
*/
public function __construct(int $operationType, string $attribute, ?array $values)
{
if (!\in_array($operationType, self::VALID_OPERATION_TYPES, true)) {
throw new UpdateOperationException(sprintf('"%s" is not a valid modification type.', $operationType));
}
if (\LDAP_MODIFY_BATCH_REMOVE_ALL === $operationType && null !== $values) {
throw new UpdateOperationException(sprintf('$values must be null for LDAP_MODIFY_BATCH_REMOVE_ALL operation, "%s" given.', get_debug_type($values)));
}
$this->operationType = $operationType;
$this->attribute = $attribute;
$this->values = $values;
}
public function toArray(): array
{
$op = [
'attrib' => $this->attribute,
'modtype' => $this->operationType,
];
if (\LDAP_MODIFY_BATCH_REMOVE_ALL !== $this->operationType) {
$op['values'] = $this->values;
}
return $op;
}
}