Korowai Framework

Korowai Framework is a framework and a library of reusable components used by the Korowai project.

Installing Korowai Framework

Use composer to install the framework or some of its components.

To install whole Korowai Framework for your project, type

1
php composer.phar require "korowai/framework:dev-master"

If you only need a component, for example korowai/ldaplib, then

1
php composer.phar require "korowai/ldaplib:dev-master"

Note

We have not released Korowai Framework yet, so it’s only installable from github.

Korowai Libraries

A korowai library is a package containing reusable code.

The Context Library

The Context library provides functionality similar to that of Python’s with-statement contexts.

Installation

1
php composer.phar require "korowai/contextlib:dev-master"

Basic Usage

The library provides with() function, whose typical use is like

1
2
3
4
5
use function Korowai\Lib\Context\with;
// ...
with($cm1, ...)(function ($arg1, ...) {
   // user's instructions to be executed within context ...
});

The arguments $cm1, ... are subject of context management. with() accepts any value as an argument but only certain types are context-managed out-of-the box. It supports most of the standard PHP resources and objects that implement ContextManagerInterface. A support for other already-existing types and classes can be added with Context Factories.

For any instance of ContextManagerInterface, its method enterContext() gets invoked just before the user-provided callback is called, and exitContext() gets invoked just after the user-provided callback returns (or throws an exception). Whatever $cm#->enterContext() returns, is passed to the user-provided callback as $arg# argument.

Simple Example

A relatively popular use of Python’s with-statement is for automatic management of opened file handles. Similar goals can be achieved with the korowai/contextlib and PHP resources. In the following example we’ll open a file to only print its contents, and a resource context manager will close it automatically when leaving the context.

The example requires the function with() to be imported to current scope

1
use function Korowai\Lib\Context\with;

Once it’s done, the following three-liner can be used to open file and read its contents. The file gets closed automatically as soon, as the user callback returns (or throws an exception).

1
2
3
with(fopen(__DIR__.'/hello.txt', 'r'))(function ($fd) {
  echo stream_get_contents($fd)."\n";
});
Trivial Value Wrapper

The TrivialValueWrapper class is a dummy context manager, which only passes value to user-provided callback. This is also a default context manager used for types/values not recognized by the context library. The following two examples are actually equivalent.

  • explicitly used TrivialValueWrapper:

    1
    2
    3
    with(new TrivialValueWrapper('argument value'))(function (string $value) {
        echo $value . "\n";
    });
    
  • TrivialValueWrapper used internally as a fallback

    1
    2
    3
    with('argument value')(function (string $value) {
        echo $value . "\n";
    });
    
Custom Context Managers

A custom context manager can be created by implementing the ContextManagerInterface. The new context manager class must implement two methods:

  • enterContext(): the value returned by this function is passed as an argument to user-provided callback when using with(),
  • exitContext(): the function accepts single argument $exception of type Throwable, which can be null (if no exception occurred within a context); the function must return boolean value indicating whether it handled (true) or not (false) the $exception.
Simple Value Wrapper

In the following example we implement simple context manager, which wraps a string and provides it as an argument to user-provided callback when using with(). Note, that there is a class TrivialValueWrapper for very similar purpose (except, it’s not restricted to strings and it doesn’t print anything).

Import symbols required for this example

1
2
use Korowai\Lib\Context\ContextManagerInterface;
use function Korowai\Lib\Context\with;

The class implementation will be rather simple. Its enterContext() and exitContext() will just print messages to inform us that the context was entered/exited.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class MyValueWrapper implements ContextManagerInterface
{
    protected $value;

    public function __construct(string $value)
    {
        $this->value = $value;
    }

    public function enterContext()
    {
        echo "MyValueWrapper::enter()\n";
        return $this->value;
    }

    public function exitContext(?\Throwable $exception = null) : bool
    {
        echo "MyValueWrapper::exit()\n";
        return false; // we didn't handle $exception
    }
}

The new context manager is ready to use. It may be tested as follows

1
2
3
with(new MyValueWrapper('argument value'))(function (string $value) {
    echo $value . "\n";
});

Obviously, the expected output will be

1
2
3
MyValueWrapper::enter()
argument value
MyValueWrapper::exit()
Context Factories

The Context Library has a concept of Context Factory (a precise name should actually be Context Manager Factory). A Context Factory is an object which takes a value and returns a new instance of ContextManagerInterface, appropriate for given value, or null if it won’t handle the value. For example, the DefaultContextFactory creates ResourceContextManager for any PHP resource and TrivialValueWrapper for any other value (except for values that are already instances of ContextManagerInterface).

The Context Library has a single stack of (custom) Context Factories (ContextFactoryStack). It’s empty by default, so initially only the DefaultContextFactory is used to generate Context Managers. A custom factory object can be pushed to the top of the stack to get precedence over other factories.

Creating custom Context Factories

A simplest way to create new Context Factory is to extend the AbstractManagedContextFactory. The new context factory must implement the getContextManager() method. The AbstractManagedContextFactory is either a Context Factory and Context Manager. When an instance of AbstractManagedContextFactory is passed to with(), it gets pushed to the top of ContextFactoryStack when entering context and popped when exiting (so the new Context Factory works for all nested contexts).

Example with custom Managed Context Factory

In the following example we’ll wrap an integer value with an object named MyCounter. Then, we’ll create a dedicated Context Manager, named MyCounterManger, to increment the counter when entering a context and decrement when exiting. Finally, we’ll provide Context Factory named MyContextFactory to recognize MyCounter objects and wrap them with MyCounterManager.

For the purpose of example we need the following symbols to be imported

1
2
3
use function Korowai\Lib\Context\with;
use Korowai\Lib\Context\AbstractManagedContextFactory;
use Korowai\Lib\Context\ContextManagerInterface;

Our counter class will be as simple as

1
2
3
4
5
6
7
8
9
class MyCounter
{
    public $value;

    public function __construct(int $value)
    {
        $this->value = $value;
    }
}

The counter manager shall just increment/decrement counter’s value and print short messages when entering/exiting a context.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyCounterManager implements ContextManagerInterface
{
    public $counter;

    public function __construct(MyCounter $counter)
    {
        $this->counter = $counter;
    }

    public function enterContext()
    {
        $this->counter->value ++;
        print("MyCounterManager::enterContext()\n");
        return $this->counter;
    }

    public function exitContext(?\Throwable $exception = null) : bool
    {
        $this->counter->value --;
        print("MyCounterManager::exitContext()\n");
        return false;
    }
}

Finally, comes the Context Factory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class MyContextFactory extends AbstractManagedContextFactory
{
    public function getContextManager($arg) : ?ContextManagerInterface
    {
        if($arg instanceof MyCounter) {
            return new MyCounterManager($arg);
        }
        return null;
    }
}

We can now push an instance of MyContextFactory to the factory stack. To push it temporarily, we’ll create two nested contexts (outer and inner), pass an instance of MyContextFactory to the outer context and do actual job in the inner context.

1
2
3
4
5
6
7
with(new MyContextFactory(), new MyCounter(0))(function ($cf, $cnt) {
    echo "before: " . $cnt->value . "\n";
    with($cnt)(function ($cnt) {
        echo "in: " . $cnt->value . "\n";
    });
    echo "after: " . $cnt->value . "\n";
});

It must be clear, that MyContextFactory is inactive in the outer with() (line 1). It only works when entering/exiting inner contexts (line 3 in the above snippet).

Following is the output from our example

1
2
3
4
5
before: 0
MyCounterManager::enterContext()
in: 1
MyCounterManager::exitContext()
after: 0
Multiple context arguments

Multiple arguments may be passed to with():

1
2
3
with($cm1, $cm2, ...)(function ($arg1, $arg2, ...) {
   # body of the user-provided callback ...
});

For every value $cm1, $cm2, …, passed to with() a corresponding value is passed as an argument to the user-provided callback. Assuming, $cm1, $cm2, …, are Context Managers, the corresponding arguments of the user-provided callback will receive

  • $arg1 = $cm1->enterContext(),
  • $arg2 = $cm2->enterContext(),

The context managers cm1, cm2, …, are invoked in the same order as they appear on the argument list to with() when entering the context (enterContext()) and in the reverse order when exiting the context (exitContext()).

Let’s use the following simple context manager to illustrate this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class MyInt implements ContextManagerInterface
{
    public $value;

    public function __construct(int $value)
    {
        $this->value = $value;
    }

    public function enterContext()
    {
        echo "enter: " . $this->value . "\n";
        return $this->value;
    }

    public function exitContext(?\Throwable $exception = null) : bool
    {
        echo "exit: " . $this->value . "\n";
        return false;
    }
}

The order or argument processing may be then illustrated by the following test

1
2
3
with(new MyInt(1), new MyInt(2), new MyInt(3))(function (int ...$args) {
    echo '$args: ' . implode(', ', $args) . "\n";
});

The output from above snippet will be

enter: 1
enter: 2
enter: 3
$args: 1, 2, 3
exit: 3
exit: 2
exit: 1
Exception Handling
Default behavior

One of the main benefits of using contexts is their “unroll” feature which works even when an exception occurs in a user-provided callback. This means, that exitContext() is invoked, even if the user’s code execution gets interrupted by an exception. To illustrate this, we’ll slightly modify the example from the section named Multiple context arguments. We’ll use same MyInt objects as context managers for all context arguments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class MyInt implements ContextManagerInterface
{
    public $value;

    public function __construct(int $value)
    {
        $this->value = $value;
    }

    public function enterContext()
    {
        echo "enter: " . $this->value . "\n";
        return $this->value;
    }

    public function exitContext(?\Throwable $exception = null) : bool
    {
        echo "exit: " . $this->value . "\n";
        return false;
    }
}

Instead of doing anything useful, we’ll just throw our custom exception MyException from the context (later):

1
2
3
class MyException extends Exception
{
}

The exception handling and unroll process may be demonstrated with the following snippet. We expect all the values 1, 2, and 3 to be printed at enter and the same numbers in reversed order printed when context exits. Finally, we should also receive MyException.

1
2
3
4
5
6
7
8
try {
    with(new MyInt(1), new MyInt(2), new MyInt(3))(function (int ...$args) {
        throw new MyException('my error message');
    });
} catch (MyException $e) {
    fprintf(STDERR, "%s\n", $e->getMessage());
    exit(1);
}

The outputs from above snippet shall be

  • stdout:

    1
    2
    3
    4
    5
    6
    enter: 1
    enter: 2
    enter: 3
    exit: 3
    exit: 2
    exit: 1
    
  • stderr:

    1
    my error message
    
Handling exceptions in exitContext

If one of the context managers returns true from its exitContext(), all the remaining context managers will receive null as $exception argument and the exception will be treated as handled (it will not be propagated to the context caller). To demonstrate this, let’s consider the following modified MyInt class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyInt implements ContextManagerInterface
{
    public $value;
    public $handle;

    public function __construct(int $value, bool $handle = false)
    {
        $this->value = $value;
        $this->handle = $handle;
    }

    public function enterContext()
    {
        echo "enter: " . $this->value . "\n";
        return $this->value;
    }

    public function exitContext(?\Throwable $exception = null) : bool
    {
        echo "exit: " . $this->value . " (" . strtolower(gettype($exception)) . ")\n";
        return $this->handle;
    }
}

The object may be configured to return true or false. What happens when one of the context managers returns true, may be explained by the following snippet

1
2
3
with(new MyInt(1), new MyInt(2), new MyInt(3, true), new MyInt(4))(function (int ...$args) {
    throw new MyException('my error message');
});

When unrolling, the objects MyInt(4) and MyInt(3, true) receive an instance of MyException as $exception, then MyInt(3, true) returns true and the remaining objects MyInt(2) and MyInt(1) receive null as $exception. The exception thrown from user-provided callback is not propagated to the outside. The code from the above snippet runs without an exception and outputs the following text

1
2
3
4
5
6
7
8
enter: 1
enter: 2
enter: 3
enter: 4
exit: 4 (object)
exit: 3 (object)
exit: 2 (null)
exit: 1 (null)

The Error Library

The Error library provides handy utilities for error flow alteration.

Installation

1
php composer.phar require "korowai/errorlib:dev-master"

Purpose

The main purpose of Error library is to provide handy utilities to control the flow of PHP errors within an application. It is designed to play nicely with our Context Library, so one can temporarily redirect PHP errors to custom handlers in one location without altering other parts of code.

Korowai Framework uses The Error Library to implement variants of PHP functions to throw predefined exceptions when the original functions trigger errors.

Basic Example

In the following example we’ll redirect errors from one invocation of a problematic function to a no-op error handler. The example uses the following functions

1
2
use function Korowai\Lib\Context\with;
use function Korowai\Lib\Error\emptyErrorHandler;

and the problematic function is

1
2
3
4
function trouble()
{
    trigger_error('you have a problem');
}

The function could normally cause some noise. For example, it could call default or an application-wide error handler. Invoking it with @ only disables error messages, but the handler is still invoked. We can prevent this by temporarily replacing original handler with our own empty handler. This is easily achieved with Contexts and EmptyErrorHandler.

1
2
3
with(emptyErrorHandler())(function ($eh) {
    trouble();
});
Custom Error Handlers

With The Error Library and Contexts you can easily use your own functions as error handlers for particular calls. A class named ErrorHandler serves the purpose.

Simple example with custom error handler

The example uses the following symbols

1
2
use function Korowai\Lib\Context\with;
use function Korowai\Lib\Error\errorHandler;

Our error handler will respond differently to user warnings and notices. Warnings will be printed to stderr, while notices will go normally to stdout.

1
2
3
4
5
6
7
8
9
function watch(int $severity, string $message, string $file, int $line) : bool
{
    if ($severity & (E_USER_WARNING|E_USER_ERROR)) {
        fprintf(STDERR, "beware, %s!\n", $message);
    } else {
        fprintf(STDOUT, "be cool, %s\n", $message);
    }
    return true;
}

The ErrorHandler object is used to enable our error handler temporarily, so it is active only within the context

1
2
3
4
5
with(errorHandler(watch::class))(function ($eh) {
    @trigger_error('the weather is nice', E_USER_NOTICE);
    @trigger_error('rain is coming', E_USER_WARNING);
});
@trigger_error('good night', E_USER_NOTICE);

The outputs from the above example are

  • stdout:
1
be cool, the weather is nice
  • stderr:
1
beware, rain is coming!

Note, that the last call to @trigger_error() (line 5) didn’t output anything.

Exception Error Handlers

Exception Error Handlers are error handlers that throw predefined exceptions in response to PHP errors. The Error Library provides ExceptionErrorHandler class to easily create exception-throwing error handlers.

A simple example with ExceptionErrorHandler

In this example we’ll set up a context to throw \ErrorException in response to PHP errors of severity E_USER_ERROR. We’ll use following two functions

1
2
use function Korowai\Lib\Context\with;
use function Korowai\Lib\Error\exceptionErrorHandler;

The exceptionErrorHandler() returns new error handler which throws a predefined exception. This handler may be enabled for a context by passing it as an argument to with(). All the necessary code is shown in the following snippet

1
2
3
4
5
6
7
8
try {
    with(exceptionErrorHandler(\ErrorException::class))(function ($eh) {
        @trigger_error('boom!', E_USER_ERROR);
    });
} catch (\ErrorException $e) {
    fprintf(STDERR, "%s:%d:ErrorException:%s\n", basename($e->getFile()), $e->getLine(), $e->getMessage());
    exit(1);
}

The output from our example is like

  • stderr

    simple_exception_thrower.php:11:ErrorException:boom!
    
Caller Error Handlers

Caller Error Handlers are useful when a function implementer wants to “blame” function’s callers for errors occurring within the function. Normally, when error is triggered with trigger_error(), the error handler receives $file and $line pointing to the line of code where the trigger_error() was invoked. This can be easily changed with Caller Error Handlers, where the developer can easily point to current function’s caller or its caller’s caller, and so on.

Simple example with Caller Error Handler

The example uses the following symbols

1
2
use function Korowai\Lib\Context\with;
use function Korowai\Lib\Error\callerErrorHandler;

Our error handler will just output $file and $line it received from CallerErrorHandler.

1
2
3
4
5
function handler(int $severity, string $message, string $file, int $line) : bool
{
    printf("error occured at %s: %d: (%s)\n", basename($file), $line, $message);
    return true;
}

We’re now going to implement a function which triggers an error, but blames its caller for this error. This may be easily done with the CallerErrorHandler class.

1
2
3
4
5
6
7
function trigger()
{
    with(callerErrorHandler(handler::class))(function ($eh) {
        printf("trigger_error() called at: %s: %d\n", basename(__file__), __line__ + 1);
        @trigger_error("error message");
    });
}

Finally, we test our function with the following code

1
2
printf("trigger() called at: %s: %d\n", basename(__file__), __line__ + 1);
trigger();

The outputs from the above example are

  • stdout:
1
2
3
trigger() called at: caller_error_handler.php: 28
trigger_error() called at: caller_error_handler.php: 21
error occured at caller_error_handler.php: 28: (error message)
Exception-throwing Caller Error Handler

The example uses the following symbols

1
2
use function Korowai\Lib\Context\with;
use function Korowai\Lib\Error\callerExceptionErrorHandler;

We’re now going to implement a function which triggers an error, but blames its caller for this error. This may be easily done with the CallerExceptionErrorHandler class.

1
2
3
4
5
6
7
function trigger()
{
    with(callerExceptionErrorHandler(\ErrorException::class))(function ($eh) {
        printf("trigger_error() called at: %s: %d\n", basename(__file__), __line__ + 1);
        @trigger_error("error message");
    });
}

Finally, we test our function with the following code

1
2
3
4
5
6
7
try {
    printf("trigger() called at: %s: %d\n", basename(__file__), __line__ + 1);
    trigger();
} catch (\ErrorException $e) {
    printf("error occured at %s: %d: (%s)\n", basename($e->getFile()), $e->getLine(), $e->getMessage());
    exit(1);
}

The outputs from the above example are

  • stdout:
1
2
3
trigger() called at: caller_error_thrower.php: 21
trigger_error() called at: caller_error_thrower.php: 13
error occured at caller_error_thrower.php: 21: (error message)

The Ldap Library

The LDAP library provides means to connect to and use an LDAP server.

Installation

1
php composer.phar require "korowai/ldaplib:dev-master"

Basic Usage

Ldap Library provides Ldap class to authenticate and query against an LDAP server.

1
use Korowai\Lib\Ldap\Ldap;

An instance of Ldap may be easily created with Ldap::createWithConfig().

1
$ldap = Ldap::createWithConfig(['uri' => 'ldap://ldap-service']);

To establish connection and authenticate, the bind() method can be used.

1
2
/* Bind to 'cn=admin,dc=example,dc=org' using password 'admin'. */
$ldap->bind('cn=admin,dc=example,dc=org', 'admin');

Once bound, we can search in the database with search() method.

1
2
/* The returned result implements ResultInterface. */
$result = $ldap->search('ou=people,dc=example,dc=org', 'objectclass=*');

The $result object returned by search() provides access to entries selected by the query. It ($result) implements the ResultInterface which, in turn, includes the standard IteratorAggregate interface. This means, you may iterate over the result entries in a usual way

1
2
3
foreach($result as $dn => $entry) {
  print($dn . " => "); print_r($entry->getAttributes());
}

Alternatively, an array of entries can be retrieved with a single call to getEntries() method

1
$entries = $result->getEntries();

By default, entries’ distinguished names (DN) are used as array keys in the returned $entries

1
$entry = $entries['uid=jsmith,ou=people,dc=example,dc=org'];

Each entry is an Entry object and contains attributes. It can me modified in memory

1
$entry->setAttribute('uidnumber', [1234]);

Note

The Ldap library uses lower-cased keys to access entry attributes. Attributes in Entry are always array-valued.

Once modified, the entry may be written back to the LDAP database with update() method.

1
$ldap->update($entry);
Ldap Configuration

Ldap instances are configured according to settings provided with $config array (an argument to Ldap::createWithConfig()). The $config array is internally passed to an adapter factory to create the supporting adapter object. Some settings are “standard” and shall be accepted by any adapter type. Other options may be specific to a particular adapter type (such as the default ExtLdap\Adapter, where the adapter-specific options are stored in $config['options']).

Common LDAP settings

The following table lists configuration settings supported by any adapter.

Configuration settings
Option Default Description
host 'localhost' Host to connect to (server IP or hostname).
uri null URI for LDAP connection. This may be specified alternativelly to host, port and encryption.
encryption 'none' Encription. Possible values: 'none', 'ssl'.
port 389 or 636 Server port to conect to. If 'encryption' is 'ssl', then 636 is used. Otherwise default 'port' is 389.
options array() An array of additional connection options (adapter-specific).
LDAP options specific to ExtLdap adapter

The $config['options'] specific to ExtLdap\Adapter are listed below. For more details see PHP function ldap_get_option() and OpenLDAP function ldap_get_option(3).

Configuration options for ExtLdap\Adapter
Option Type Description
deref string|int

Specifies how alias dereferencing is done when performing a search. The option’s value can be specified as one of the following constants:

  • 'never' (LDAP_DEREF_NEVER): aliases are never dereferenced,
  • searching (LDAP_DEREF_SEARCHING): aliases are dereferenced in subordinates of the base object, but not in locating the base object of the search,
  • 'finding' (LDAP_DEREF_FINDING): aliases are only dereferenced when locating the base object of the search,
  • 'always' (LDAP_DEREF_ALWAYS): aliases are dereferenced both in searching and in locating the base object of the search.
sizelimit int Specifies a size limit (number of entries) to use when performing searches. The number should be a non-negative integer. sizelimit of zero (0) specifies a request for unlimited search size. Please note that the server may still apply any server-side limit on the amount of entries that can be returned by a search operation.
timelimit int Specifies a time limit (in seconds) to use when performing searches. The number should be a non-negative integer. timelimit of zero (0) specifies unlimited search time to be used. Please note that the server may still apply any server-side limit on the duration of a search operation.
network_timeout int Specifies the timeout (in seconds) after which the poll(2)/select(2) following a connect(2) returns in case of no activity.
protocol_version int Specifies what version of the LDAP protocol should be used. Allowed values are 2 and 3. Default is: 3.
error_number int Sets/gets the LDAP result code associated to the handle.
referrals bool Determines whether the library should implicitly chase referrals or not.
restart bool Determines whether the library should implicitly restart connections.
host_name string Sets/gets a space-separated list of hosts to be contacted by the library when trying to establish a connection. This is now deprecated in favor of uri.
error_string string Sets/gets a string containing the error string associated to the LDAP handle. This option is now known as diagnostic_message (LDAP_OPT_DIAGNOSTIC_MESSAGE).
diagnostic_message string Sets/gets a string containing the error string associated to the LDAP handle. This option was formerly known as error_string (LDAP_OPT_ERROR_STRING).
matched_dn string Sets/gets a string containing the matched DN associated to the LDAP handle.
server_controls array Sets/gets the server-side controls to be used for all operations. This is now deprecated as modern LDAP C API provides replacements for all main operations which accepts server-side controls as explicit arguments; see for example ldap_search_ext(3), ldap_add_ext(3), ldap_modify_ext(3) and so on.
client_controls array Sets/gets the client-side controls to be used for all operations. This is now deprecated as modern LDAP C API provides replacements for all main operations which accepts client-side controls as explicit arguments; see for example ldap_search_ext(3), ldap_add_ext(3), ldap_modify_ext(3) and so on.
keepalive_idle int Sets/gets the number of seconds a connection needs to remain idle before TCP starts sending keepalive probes.
keepalive_probes int Sets/gets the maximum number of keepalive probes TCP should send before dropping the connection.
keepalive_interval int Sets/gets the interval in seconds between individual keepalive probes.
sasl_mech string Gets the SASL mechanism.
sasl_realm string Gets the SASL realm.
sasl_authcid string Gets the SASL authentication identity.
sasl_authzid string Gets the SASL authorization identity.
tls_cacertdir string Sets/gets the path of the directory containing CA certificates.
tls_cacertfile string Sets/gets the full-path of the CA certificate file.
tls_certfile string Sets/gets the full-path of the certificate file.
tls_cipher_suite string Sets/gets the allowed cipher suite.
tls_crlcheck string|int

Sets/gets the CRL evaluation strategy, one of

  • 'none' (LDAP_OPT_X_TLS_CRL_NONE),
  • 'peer' (LDAP_OPT_X_TLS_CRL_PEER),
  • 'all' (LDAP_OPT_X_TLS_CRL_ALL).
tls_crlfile string Sets/gets the full-path of the CRL file.
tls_dhfile string Gets/sets the full-path of the file containing the parameters for Diffie-Hellman ephemeral key exchange.
tls_keyfile string Sets/gets the full-path of the certificate key file.
tls_protocol_min int Sets/gets the minimum protocol version.
tls_random_file string Sets/gets the random file when /dev/random and /dev/urandom are not available.
tls_require_cert string|int

Sets/gets the peer certificate checking strategy, one of

  • 'never' (LDAP_OPT_X_TLS_NEVER),
  • 'hard' (LDAP_OPT_X_TLS_HARD),
  • 'demand' (LDAP_OPT_X_TLS_DEMAND),
  • 'allow' (LDAP_OPT_X_TLS_ALLOW),
  • 'try' (LDAP_OPT_X_TLS_TRY).
Ldap Exceptions

Ldap Library uses exceptions to report most of errors. Exceptions used by The Ldap Library are defined in Korowai\Lib\Ldap\Exception namespace. The following exception classes are currently defined:

Ldap library’s exceptions
Exception Base Exception Thrown when
AttributeException OutOfRangeException accessing nonexistent attribute of an LDAP Entry
LdapException ErrorException an error occurs during an LDAP operation
AttributeException

Derived from OutOfRangeException. It’s being thrown when accessing nonexistent attribute of an LDAP Entry. For example

1
        $entry->getAttribute('inexistent');
LdapException

Derived from ErrorException. It’s being thrown when an LDAP operation fails. The exception message and code are taken from the LDAP backend.

1
2
3
4
5
6
try {
    $ldap->search('dc=inexistent,dc=org', 'cn=admin');
} catch (LdapException $e) {
    fprintf(STDERR, "LdapException(0x%x): %s\n", $e->getCode(), $e->getMessage());
    exit(1);
}

The output from above example is the following

1
LdapException(0x20): No such object

To handle particular LDAP errors in an application, exception code may be used

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
try {
    $ldap->search('dc=inexistent,dc=org', 'cn=admin');
} catch (LdapException $e) {
    if ($e->getCode() == 0x20) { /* No such object */
        fprintf(STDERR, "Warning(0x%x): %s\n", $e->getCode(), $e->getMessage());
        exit(2);
    } else {
        fprintf(STDERR, "LdapException(0x%x): %s\n", $e->getCode(), $e->getMessage());
        exit(1);
    }
}

The output from above example is the following

1
Warning(0x20): No such object

Standard LDAP result codes (including error codes) are defined in several documents including RFC 4511, RFC 3928, RFC 3909, RFC 4528, and RFC 4370. An authoritative source of LDAP result codes is the IANA registry. A useful list of LDAP return codes may also be found on LDAP Wiki.

Ldap Adapters

The Ldap Library uses adapters to interact with the actual LDAP implementation (client library). An adapter is a class that converts the interface of that particular implementation to AdapterInterface. This pattern allows for different LDAP implementations to be used by The Ldap Library in a pluggable manner. The Ldap Library itself provides an adapter named ExtLdap which makes use of the standard PHP ldap extension.

Each Ldap instance wraps an instance of AdapterInterface (the adapter) and interacts with particular LDAP implementation through this adapter. The adapter is feed to Ldap’s constructor when it’s being created. The whole process of adapter instantiation is done behind the scenes.

Adapter Factory

An adapter class is accompanied with its adapter factory. This configurable object creates adapter instances. Adapter factories implement AdapterFactoryInterface which defines two methods: configure() and createAdapter(). New adapter instances are created with createAdapter() according to configuration options provided earlier to configure().

Adapter factory may be specified when creating an Ldap instance. For this purpose, a preconfigured instance of the AdapterFactoryInterface shall be provided to Ldap’s static method createWithAdapterFactory():

1
2
3
4
5
6
use Korowai\Lib\Ldap\Ldap;
use Korowai\Lib\Ldap\Adapter\ExtLdap\AdapterFactory;

$config = ['uri' => 'ldap://ldap-service'];
$factory = new AdapterFactory($config);
$ldap = Ldap::createWithAdapterFactory($factory);

Alternatively, factory class name may be passed to createWithConfig() method:

1
2
3
4
5
use Korowai\Lib\Ldap\Ldap;
use Korowai\Lib\Ldap\Adapter\ExtLdap\AdapterFactory;

$config = ['uri' => 'ldap://ldap-service'];
$ldap = Ldap::createWithConfig($config, AdapterFactory::class);

In this case, a temporary instance of adapter factory is created internally, configured with $config and then used to create the actual adapter instance for Ldap.

Mocking Ldap objects

Unit-testing applications that use The Ldap Library can be troublesome. The code under test may depend on ldap library’s interfaces, such as SearchQueryInterface. A common practice for unit-testing is to not depend on real services, so we can’t just use actual implementations, such as ExtLdap\SearchQuery, which operate on real LDAP databases.

Mocking is a technique of replacing actual object with fake ones called mocks or stubs. It’s applicable also to our case, but creating mocks for objects/interfaces of The Ldap Library becomes complicated when it comes to higher-level interfaces such as the SearchQueryInterface.

Consider the following function to be unit-tested

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function getPosixAccounts(SearchQueryInterface $query) : array
{
    function isPosixAccount(ResultEntryInterface $entry) : bool
    {
        $attributes = $entry->getAttributes();
        $objectclasses = array_map('strtolower', $attributes['objectclass'] ?? []);
        return in_array('posixaccount', $objectclasses);
    }

    $result = $query->getResult();
    $users = [];
    foreach ($result->getResultEntryIterator() as $entry) {
        if (isPosixAccount($entry)) {
            $users[] = $entry;
        }
    }
    return $users;
}

The expected behavior is that getPosixAccounts() executes $query and extracts posixAccount entries from its result. From the code we see immediately, that our unit-test needs to create a mock for SearchQueryInterface. The mock shall provide getResult() method returning an instance of ResultInterface (another mock?) having getResultEntryIterator() method that shall return an instance of ResultEntryIteratorInterface (yet another mock?) and so on. Quite complicated as for single unit-test, isn’t it?

To facilitate unit-testing and mocking, The Ldap Library provides a bunch of classes for “fake objects” under the Korowai\Lib\Ldap\Adapter\Mock namespace. For the purpose of our example, an instance of Result class (from the above namespace) may be created

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
        // this is used instead of a long chain of mocks...
        $result = Result::make([
            [
                'dn' => 'cn=admin,dc=org',
                'cn' => 'admin',
                'objectClass' => ['person']
            ], [
                'dn' => 'uid=jsmith,dc=org',
                'uid' => 'jsmith',
                'objectClass' => ['posixAccount']
            ], [
                'dn' => 'uid=gbrown,dc=org',
                'uid' => 'gbrown',
                'objectClass' => ['posixAccount']
            ],
        ]);

and used as a return value of the mocked SearchQueryInterface::getResult() method.

1
2
3
4
5
6
        $queryMock = $this->getMockBuilder(SearchQueryInterface::class)
                          ->getMockForAbstractClass();
        $queryMock->expects($this->once())
                  ->method('getResult')
                  ->with()
                  ->willReturn($result);

This significantly reduces the boilerplate of mocking the SearchQueryInterface (we created one mock and one fake object instead of a chain of four mocks). The full example is the following

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/* [use] */
use PHPUnit\Framework\TestCase;
use Korowai\Lib\Ldap\Adapter\SearchQueryInterface;
use Korowai\Lib\Ldap\Adapter\ResultEntryInterface;
use Korowai\Lib\Ldap\Adapter\Mock\Result;
/* [/use] */

/* [functions] */
function getPosixAccounts(SearchQueryInterface $query) : array
{
    function isPosixAccount(ResultEntryInterface $entry) : bool
    {
        $attributes = $entry->getAttributes();
        $objectclasses = array_map('strtolower', $attributes['objectclass'] ?? []);
        return in_array('posixaccount', $objectclasses);
    }

    $result = $query->getResult();
    $users = [];
    foreach ($result->getResultEntryIterator() as $entry) {
        if (isPosixAccount($entry)) {
            $users[] = $entry;
        }
    }
    return $users;
}
/* [/functions] */

/* [testcase] */
class TestGetPosixAccounts extends TestCase
{
    public function test()
    {
        /* [result] */
        // this is used instead of a long chain of mocks...
        $result = Result::make([
            [
                'dn' => 'cn=admin,dc=org',
                'cn' => 'admin',
                'objectClass' => ['person']
            ], [
                'dn' => 'uid=jsmith,dc=org',
                'uid' => 'jsmith',
                'objectClass' => ['posixAccount']
            ], [
                'dn' => 'uid=gbrown,dc=org',
                'uid' => 'gbrown',
                'objectClass' => ['posixAccount']
            ],
        ]);
        /* [/result] */

        /* [queryMock] */
        $queryMock = $this->getMockBuilder(SearchQueryInterface::class)
                          ->getMockForAbstractClass();
        $queryMock->expects($this->once())
                  ->method('getResult')
                  ->with()
                  ->willReturn($result);
        /* [/queryMock] */

        $entries = getPosixAccounts($queryMock);

        $this->assertCount(2, $entries);
        $this->assertInstanceOf(ResultEntryInterface::class, $entries[0]);
        $this->assertInstanceOf(ResultEntryInterface::class, $entries[1]);
        $this->assertSame('uid=jsmith,dc=org', $entries[0]->getDn());
        $this->assertSame(['uid' => ['jsmith'], 'objectclass' => ['posixAccount']], $entries[0]->getAttributes());
        $this->assertSame('uid=gbrown,dc=org', $entries[1]->getDn());
        $this->assertSame(['uid' => ['gbrown'], 'objectclass' => ['posixAccount']], $entries[1]->getAttributes());
    }
}
/* [/testcase] */

/* [test] */
$testCase = new TestGetPosixAccounts;
$testCase->test();
/* [/test] */
Predefined fake objects

The Result object, used in previous example, is an example of what we’ll call “fake objects”. A fake object is an implementation of particular interface, which imitates actual implementation, except the fake object does not call any actual LDAP implementation (such as the PHP ldap extension). For example, Result implements ResultInterface providing two iterators, one over a collection of ResultEntry objects and the other over ResultReference objects. Once configured with arrays of entries and references, the Result, behaves exactly as real implementation of ResultInterface would.

Below is a list of interfaces and their fake-object implementations.

Fake Objects
Interface Fake Object
ResultInterface Result
ResultEntryInterface ResultEntry
ResultReferenceInterface ResultReference
ResultEntryIteratorInterface ResultEntryIterator
ResultReferenceIteratorInterface ResultReferenceIterator
ResultAttributeIteratorInterface ResultAttributeIterator
ResultReferralIteratorInterface ResultReferralIterator

Developer Guide

This guide is intended for Korowai Framework contributors.

Indices and tables