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 fallback1 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 usingwith()
,exitContext()
: the function accepts single argument$exception
of typeThrowable
, which can benull
(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.
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).
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:
|
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
|
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
|
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:
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.