From 8d1a310921e4721e48a12e4ba54ac8601d359388 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Mon, 1 Mar 2021 16:32:48 +0000 Subject: [PATCH] - First commit, User loading and Authentication are working --- composer.json | 26 ++++ src/Adapter/Ldap.php | 165 +++++++++++++++++++++ src/Common/LdapObject.php | 174 ++++++++++++++++++++++ src/ConnectionAdapter.php | 70 +++++++++ src/Entity/Computer.php | 10 ++ src/Entity/Container.php | 10 ++ src/Entity/Field/Datetime.php | 12 ++ src/Entity/User.php | 130 ++++++++++++++++ src/EntityTrait.php | 48 ++++++ src/Query/Filter.php | 148 +++++++++++++++++++ src/Query/Limit.php | 22 +++ src/Query/Select.php | 40 +++++ src/QueryBuilder.php | 236 ++++++++++++++++++++++++++++++ src/Repository.php | 30 ++++ src/Repository/ConditionTrait.php | 153 +++++++++++++++++++ 15 files changed, 1274 insertions(+) create mode 100644 composer.json create mode 100644 src/Adapter/Ldap.php create mode 100644 src/Common/LdapObject.php create mode 100644 src/ConnectionAdapter.php create mode 100644 src/Entity/Computer.php create mode 100644 src/Entity/Container.php create mode 100644 src/Entity/Field/Datetime.php create mode 100644 src/Entity/User.php create mode 100644 src/EntityTrait.php create mode 100644 src/Query/Filter.php create mode 100644 src/Query/Limit.php create mode 100644 src/Query/Select.php create mode 100644 src/QueryBuilder.php create mode 100644 src/Repository.php create mode 100644 src/Repository/ConditionTrait.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..826f858 --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "mcnd/ulmus-ldap", + "description": "A simple LDAP extension for Ulmus.", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Dave Mc Nicoll", + "email": "mcndave@gmail.com" + } + ], + "require": { + "mcnd/ulmus": "dev-master" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/mcNdave/ulmus.git" + } + ], + "autoload": { + "psr-4": { + "Ulmus\\Ldap\\": "src/" + } + } +} diff --git a/src/Adapter/Ldap.php b/src/Adapter/Ldap.php new file mode 100644 index 0000000..2fe03f1 --- /dev/null +++ b/src/Adapter/Ldap.php @@ -0,0 +1,165 @@ +hosts = [ $host ]; + } + + if ($baseDn) { + $this->baseDn = $baseDn; + } + + if ($username) { + $this->username = $username; + } + + if ($password) { + $this->password = $password; + } + + if ($accountSuffix) { + $this->accountSuffix = $accountSuffix; + } + } + + public function authenticate(string $username, string $password) : bool + { + $this->ldapObject = $this->getLdapObject(); + + $bind = $this->ldapObject->bind($username, $password); + + return $bind; + } + + public function connect() : object + { + $this->ldapObject = $this->getLdapObject(); + + $this->bindUser(); + + return $this->ldapObject; + } + + public function bindUser() : void + { + if ( ! $this->ldapObject->bind($this->username, $this->password) ) { + throw new \Exception("LDAP bind failed with given user $usr."); + } + } + + protected function getLdapObject() : LdapObject + { + $ldapObject = new LdapObject(); + + $ldapObject->connect($this->hosts[0], $this->baseDn); + + $ldapObject->setOptions([ + \LDAP_OPT_PROTOCOL_VERSION => $this->version, + \LDAP_OPT_REFERRALS => 0, + ]); + + if ( isset($this->pathCertCrt) && isset($this->pathCertKey) ) { + $ldapObject->setOptions([ + \LDAP_OPT_X_TLS_CERTFILE => $this->pathCertCrt, + \LDAP_OPT_X_TLS_KEYFILE => $this->pathCertKey, + ], false); + + $ldapObject->startTLS(); + } + + return $ldapObject; + } + + public function buildDataSourceName() : string + { + return ""; + } + + public function setup(array $configuration) : void + { + $configuration = array_change_key_case($configuration, \CASE_LOWER); + + if ( false === ( $this->hosts = $configuration['hosts'] ?? false ) ) { + throw new AdapterConfigurationException("Your `host` setting is missing. It is a mandatory parameter for this driver."); + } + elseif ( false === ( $this->baseDn = $configuration['base_dn'] ?? false ) ) { + throw new AdapterConfigurationException("Your `base_dn` setting is missing. The adapter won't connect without it."); + } + elseif ( false === ( $this->username = $configuration['username'] ?? false ) ) { + throw new AdapterConfigurationException("Your `username` is missing from your configuration array"); + } + elseif ( false === ( $this->password = $configuration['password'] ?? false ) ) { + throw new AdapterConfigurationException("Your `password` is missing from your configuration array"); + } + elseif ( false === ( $this->accountSuffix = $configuration['account_suffix'] ?? false ) ) { + throw new AdapterConfigurationException("Your `account_suffix` is missing from your configuration array"); + } + + /* + if ( false !== ( $configuration['app'] ?? false ) ) { + $this->app = $configuration['app']; + } + */ + + } + + public function escapeIdentifier(string $segment, int $type, string $ignore = "") : string + { + switch($type) { + case static::IDENTIFIER_DN: + return ldap_escape($segment, $ignore, LDAP_ESCAPE_DN); + + case static::IDENTIFIER_FIELD: + case static::IDENTIFIER_FILTER: + return ldap_escape($segment, $ignore, LDAP_ESCAPE_FILTER); + + default: + return ldap_escape($segment, $ignore); + } + } + + public function defaultEngine(): ? string + { + return null; + } +} \ No newline at end of file diff --git a/src/Common/LdapObject.php b/src/Common/LdapObject.php new file mode 100644 index 0000000..e882770 --- /dev/null +++ b/src/Common/LdapObject.php @@ -0,0 +1,174 @@ +connection) and ldap_close($this->connection); + } + + public function connect(string $host, string $baseDn) : bool + { + $this->connection = ldap_connect($host); + + $this->dn = $baseDn; + + return $this->connection !== false; + } + + public function setOptions(array $options, $useConnectionRessource = true) : void + { + foreach($options as $field => $value) { + ldap_set_option($useConnectionRessource ? $this->connection : null, $field, $value); + } + } + + public function startTLS() : void + { + ldap_start_tls($this->connection); + } + + public function bind(? string $dn, ? string $password) : bool + { + if ($this->binded) { + throw new \Exception("LdapObject is already binded with a user. Use the unbind() method to release it."); + } + + return $this->binded = ldap_bind($this->connection, $dn, $password); + } + + public function unbind() : void + { + if ($this->binded) { + $this->binded = ! ldap_unbind($this->connection); + } + } + + public function select(array $filter, array $fields = []) + { + static::$dump && call_user_func_array(static::$dump, [ $sql, $parameters ]); + + $this->search = ldap_search($this->connection, $this->dn, $filter['filters'], $filter['fields'], 0, $filter['limit'] ?? -1); + + $this->rowCount = $this->bufferedRows = ldap_count_entries($this->connection, $this->search); + + return $this; + } + + public function fetch() /* : bool|array */ + { + if (! $this->search ) { + throw new \Exception('Impossible to fetch from LdapObject from which select() was not called first.'); + } + + if ( ! $this->bufferedRows ) { + return false; + } + if ( $this->rowCount === $this->bufferedRows ) { + $result = ldap_first_entry($this->connection, $this->search); + } + else { + $result = ldap_next_entry($this->connection, $this->search); + } + + if ($result) { + $this->bufferedRows--; + + $dataset = []; + + if ( $attributes = ldap_get_attributes($this->connection, $result) ) { + for ($i = 0; $i < $attributes['count']; $i++) { + $key = $attributes[$i]; + + $dataset[strtolower($key)] = $attributes[$key][0]; + } + } + + return $dataset; + } + + return false; + } + + public function rowCount() : int + { + return $this->rowCount; + } + + public function fetchAll() + { + return ldap_get_entries($this->connection, $result); + } + + public function closeCursor() : void {} + + public function runQuery(string $sql, array $parameters = []) + { + static::$dump && call_user_func_array(static::$dump, [ $sql, $parameters ]); + + try { + if (false !== ( $statement = $this->prepare($sql) )) { + return $this->execute($statement, $parameters, true); + } + } + catch (\PDOException $e) { + throw new \PdoException($e->getMessage() . " `$sql` with data:" . json_encode($parameters)); + } + + return null; + } + + public function execute(PDOStatement $statement, array $parameters = [], bool $commit = true) + { + try { + if ( ! $this->inTransaction() ) { + $this->beginTransaction(); + } + + if (empty($parameters) ? $statement->execute() : $statement->execute($parameters)) { + $statement->lastInsertId = $this->lastInsertId(); + + if ( $commit ) { + $this->commit(); + } + + return $statement; + } + else { + throw new \PDOException($statement->errorCode() . " - " . json_encode($statement->errorInfo())); + } + } + catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + catch (\Throwable $e) { + if ( function_exists("debogueur") ) { + debogueur($statement, $parameters, $commit); + } + + throw $e; + } + + return null; + } +} diff --git a/src/ConnectionAdapter.php b/src/ConnectionAdapter.php new file mode 100644 index 0000000..6d5f234 --- /dev/null +++ b/src/ConnectionAdapter.php @@ -0,0 +1,70 @@ +configuration['connections'][$this->name] ?? []; + + if ( false !== ( $adapterName = $connection['adapter'] ?? false ) ) { + $this->adapter = $this->instanciateAdapter($adapterName); + } + else { + $this->adapter = new Adapter\Ldap(); + } + + $this->adapter->setup($connection); + } + + public function getConfiguration() : array + { + return $this->configuration['connections'][$this->name]; + } + + /** + * Connect the adapter + * @return self + */ + public function connect() : self + { + $this->connection = $this->adapter->connect(); + + return $this; + } + + public function connector() : object + { + return $this->connection; + } + + public function adapter() : AdapterInterface + { + return $this->adapter; + } + + /** + * Instanciate an adapter which interact with the data source + * @param string $name An Ulmus adapter or full class name implementing AdapterInterface + * @return AdapterInterface + */ + protected function instanciateAdapter($name) : AdapterInterface + { + $class = substr($name, 0, 2) === "\\" ? $name : "\\Ulmus\\Adapter\\$name"; + + return new $class(); + } +} diff --git a/src/Entity/Computer.php b/src/Entity/Computer.php new file mode 100644 index 0000000..9a7d14c --- /dev/null +++ b/src/Entity/Computer.php @@ -0,0 +1,10 @@ + 'givenname') + */ + public string $firstname; + + /** + * @Field('name' => 'sn') + */ + public string $lastname; + + /** + * @Field + */ + public string $displayName; + + /** + * @Field + */ + public string $name; + + /** + * @Field + */ + public string $userPrincipalName; + + /** + * @Field + */ + public string $description; + + /** + * @Field('name' => "st") + */ + public string $state; + + /** + * @Field('name' => 'physicalDeliveryOfficeName') + */ + public string $officeName; + + /** + * @Field + */ + public string $company; + + /** + * @Field + */ + public string $department; + + /** + * @Field + */ + public string $title; + + /** + * @Field + */ + public string $userAccountControl; + + /** + * @Field + */ + public string $extensionAttribute1; + + /** + * @Field + */ + public string $extensionAttribute2; + + /** + * @Field + */ + public string $extensionAttribute3; + + /** + * @Field + */ + public string $extensionAttribute4; + + /** + * @Field + */ + public int $logonCount; + + /** + * @Field + */ + public Datetime $lastLogonDate; + + /** + * @Field('name' => 'createTimeStamp') + */ + public Datetime $createdAt; + + /** + * @Field('name' => 'modifyTimeStamp') + */ + public Datetime $updatedAt; + + public function __toString() : string + { + return $this->displayName; + } +} \ No newline at end of file diff --git a/src/EntityTrait.php b/src/EntityTrait.php new file mode 100644 index 0000000..15c8083 --- /dev/null +++ b/src/EntityTrait.php @@ -0,0 +1,48 @@ +"; + const CONDITION_AND = "&"; + const CONDITION_OR = "|"; + const CONDITION_NOT = "!"; + const COMPARISON_IN = "IN"; + const COMPARISON_IS = "IS"; + const COMPARISON_NULL = "NULL"; + + public array $conditionList; + + public QueryBuilderInterface $queryBuilder; + + public ? Filter $parent = null; + + public string $condition = self::CONDITION_AND; + + public function __construct(? QueryBuilderInterface $queryBuilder, $condition = self::CONDITION_AND) + { + $this->queryBuilder = $queryBuilder; + $this->condition = $condition; + $this->parent = $queryBuilder->Filter ?? null; + } + + public function add($field, $value, string $operator, string $condition, bool $not = false) : self + { + $this->conditionList[] = [ + $field, + $value, + $operator ?: $this->queryBuilder->conditionOperator, + $condition, + $not + ]; + + return $this; + } + + public function render(bool $skipToken = false) : array + { + $stack = []; + + foreach ($this->conditionList ?? [] as $key => $item) { + if ( $item instanceof Filter ) { + if ( $item->conditionList ?? false ) { + $stack[] = ( $key !== 0 ? "{$item->condition} " : "" ) . "(" . implode('', $item->render($skipToken)) . ")"; + } + } + else { + list($field, $value, $operator, $condition, $not) = $item; + $stack[] = $latest = $this->FilterCondition($field, $value, $operator, $key !== 0 ? $condition : "", $not); + } + } + + return [ + 'filters' => implode('', $stack) + ]; + } + + protected function FilterCondition($field, $value, string $operator = self::OPERATOR_EQUAL, string $condition = self::CONDITION_AND, bool $not = false) { + return new class($this->queryBuilder, $field, $value, $operator, $condition, $not) { + + public $value; + public bool $not = false; + public string $field; + public string $operator; + public string $condition; + public QueryBuilderInterface $queryBuilder; + + protected string $content = ""; + + public function __construct(QueryBuilderInterface $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) { + $this->queryBuilder = $queryBuilder; + $this->field = $field; + $this->value = $value; + $this->condition = $condition; + $this->operator = $operator; + $this->not = $not; + } + + public function render() : string + { + $value = $this->value(); + + return $this->content ?: $this->content = implode("", array_filter([ + "(", + $this->condition, + $this->not ? Filter::CONDITION_NOT : "", + $this->field, + $this->operator(), + $value, + ")", + ])); + } + + protected function operator() : string + { + if ( is_array($this->value) ) { + return (in_array($this->operator, [ '!=', '<>' ]) ? Filter::CONDITION_NOT . " " : "") . Filter::COMPARISON_IN; + } + + return $this->operator; + } + + protected function value() + { + if ( is_array($this->value) ) { + $stack = []; + + foreach($this->value as $item) { + $stack[] = $this->filterValue($item); + } + + return "(" . implode(", ", $stack) . ")"; + } + + return $this->filterValue($this->value); + } + + protected function filterValue($value) + { + if ( $value === null ) { + throw new \Exception("NULL is not an acceptable value for filter request."); + } + elseif ( is_object($value) && ( $value instanceof EntityField ) ) { + return $value->name(); + } + else { + return $value; + } + } + + public function __toString() : string + { + return $this->render(); + } + }; + } +} diff --git a/src/Query/Limit.php b/src/Query/Limit.php new file mode 100644 index 0000000..ce6c1b8 --- /dev/null +++ b/src/Query/Limit.php @@ -0,0 +1,22 @@ +limit = $limit; + + return $this; + } + + public function render() : array + { + return [ + 'limit' => $this->limit, + ]; + } +} diff --git a/src/Query/Select.php b/src/Query/Select.php new file mode 100644 index 0000000..9fab5f5 --- /dev/null +++ b/src/Query/Select.php @@ -0,0 +1,40 @@ +fields = is_array($fields) ? $fields : [ $fields ]; + + return $this; + } + + public function add($fields) : self + { + if ( is_array($fields) ) { + $this->fields = array_merge($this->fields, $fields); + } + else { + $this->fields[] = $fields; + } + + return $this; + } + + public function render() : array + { + return [ + 'fields' => $this->fields + ]; + } +} diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php new file mode 100644 index 0000000..e203585 --- /dev/null +++ b/src/QueryBuilder.php @@ -0,0 +1,236 @@ +getFragment(Query\Select::class) ) ) { + $select->add($field); + } + else { + $select = new Query\Select(); + $select->set($field); + $this->push($select); + } + + return $this; + } + + public function from(string $table, ? string $alias, ? string $schema) : self + { + return $this; + } + + public function update(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self + { + return $this; + } + + public function delete() : self + { + if ( ! $this->getFragment(Ulmus\Query\Delete::class) ) { + $this->push(new Ulmus\Query\Delete()); + } + + return $this; + } + + public function open(string $condition = Ulmus\Query\Where::CONDITION_AND) : self + { + if ( null !== ($this->where ?? null) ) { + $this->where->conditionList[] = $new = new Query\Filter($this, $condition); + $this->where = $new; + } + else { + $this->where = new Query\Filter($this, $condition); + $this->push($this->where); + $this->where->conditionList[] = $new = new Query\Filter($this, $condition); + $this->where = $new; + } + + return $this; + } + + public function close() : self + { + if ( null !== ($this->where ?? null) && $this->where->parent ) { + + # if an enclosure was opened, and nothing done, we must remove the unused node + if ( empty($this->where->conditionList) && (count($this->where->parent->conditionList) === 1) ) { + unset($this->where->parent->conditionList); + } + + $this->where = $this->where->parent; + } + + return $this; + } + + public function where(/* stringable*/ $field, $value, string $operator = Ulmus\Query\Where::OPERATOR_EQUAL, string $condition = Ulmus\Query\Where::CONDITION_AND, bool $not = false) : self + { + # Empty IN case + if ( [] === $value ) { + return $this; + } + + if ( $this->where ?? false ) { + $where = $this->where; + } + elseif ( null === ( $where = $this->getFragment(Query\Filter::class) ) ) { + $this->where = $where = new Query\Filter($this); + $this->push($where); + } + + $this->whereConditionOperator = $operator; + + $where->add($field, $value, $operator, $condition, $not); + + return $this; + } + + public function notWhere($field, $value, string $operator = Ulmus\Query\Where::CONDITION_AND) : self + { + return $this->where($field, $value, $operator, true); + } + + public function limit(int $value) : self + { + if ( null === $limit = $this->getFragment(Ulmus\Query\Limit::class) ) { + $limit = new Query\Limit(); + $this->push($limit); + } + + $limit->set($value); + + return $this; + } + + public function offset(int $value) : self + { + if ( null === $offset = $this->getFragment(Ulmus\Query\Offset::class) ) { + $offset = new Ulmus\Query\Offset(); + $this->push($offset); + } + + $offset->set($value); + + return $this; + } + + public function orderBy(string $field, ? string $direction = null) : self + { + return $this; + } + + public function create(array $fieldlist, string $table, ? string $database = null, ? string $schema = null) : self + { + return $this; + } + + public function push(Ulmus\Query\Fragment $queryFragment) : self + { + $this->queryStack[] = $queryFragment; + + return $this; + } + + public function pull(Ulmus\Query\Fragment $queryFragment) : self + { + return array_shift($this->queryStack); + } + + public function render(bool $skipToken = false) : array + { + $stack = []; + + foreach($this->queryStack as $fragment) { + $stack = array_merge($stack, (array) $fragment->render($skipToken)); + } + + return $stack; + } + + public function reset() : void + { + $this->parameters = $this->values = $this->queryStack = []; + $this->whereConditionOperator = Ulmus\Query\Where::CONDITION_AND; + $this->havingConditionOperator = Ulmus\Query\Where::CONDITION_AND; + $this->parameterIndex = 0; + + unset($this->where, $this->having); + } + + public function getFragment(string $class, int $index = 0) : ? Ulmus\Query\Fragment + { + foreach($this->queryStack as $item) { + if ( get_class($item) === $class ) { + if ( $index-- === 0 ) { + return $item; + } + } + } + + return null; + } + + public function removeFragment(Ulmus\Query\Fragment $fragment) : void + { + foreach($this->queryStack as $key => $item) { + if ( $item === $fragment ) { + unset($this->queryStack[$key]); + } + } + } + + public function __toString() : string + { + return $this->render(); + } + + public function addParameter($value, string $key = null) : string + { + if ( $this->parent ?? false ) { + return $this->parent->addParameter($value, $key); + } + + if ( $key === null ) { + $key = ":p" . $this->parameterIndex++; + } + + $this->parameters[$key] = $value; + + return $key; + } + + public function addValues(array $values) : void + { + $this->values = $values; + } +} diff --git a/src/Repository.php b/src/Repository.php new file mode 100644 index 0000000..8971229 --- /dev/null +++ b/src/Repository.php @@ -0,0 +1,30 @@ +queryBuilder = new QueryBuilder(); + } + + protected function selectSqlQuery() : self + { + if ( null === $this->queryBuilder->getFragment(Query\Select::class) ) { + $this->select(array_keys($this->entityResolver->fieldList(EntityResolver::KEY_COLUMN_NAME))); + } + + return $this; + } + +} \ No newline at end of file diff --git a/src/Repository/ConditionTrait.php b/src/Repository/ConditionTrait.php new file mode 100644 index 0000000..7f2fc7c --- /dev/null +++ b/src/Repository/ConditionTrait.php @@ -0,0 +1,153 @@ +queryBuilder->open($condition); + + return $this; + } + + public function orOpen() : self + { + return $this->open(Query\Where::CONDITION_OR); + } + + public function close() : self + { + $this->queryBuilder->close(); + + return $this; + } + + public function where($field, $value, string $operator = Query\Where::OPERATOR_EQUAL, $condition = Query\Where::CONDITION_AND) : self + { + $this->queryBuilder->where($field, $value, $operator, $condition); + + return $this; + } + + public function wheres(array $fieldValues, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + foreach($fieldValues as $field => $value) { + $this->where($field, $value, $operator); + } + + return $this; + } + + public function and($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + return $this->where($field, $value, $operator); + } + + public function or($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_OR); + + return $this; + } + + public function notWhere($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self + { + $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_AND, true); + + return $this; + } + + public function orNot($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self + { + $this->queryBuilder->notWhere($field, $value, $operator, Query\Where::CONDITION_OR, true); + + return $this; + } + + public function having($field, $value, string $operator = Query\Where::OPERATOR_EQUAL, $condition = Query\Where::CONDITION_AND) : self + { + $this->queryBuilder->having($field, $value, $operator, $condition); + + return $this; + } + + public function orHaving($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + $this->queryBuilder->having($field, $value, $operator, Query\Where::CONDITION_OR); + + return $this; + } + + public function notHaving($field, $value, string $operator = Query\Where::OPERATOR_NOT_EQUAL) : self + { + $this->queryBuilder->having($field, $value, $operator, Query\Where::CONDITION_AND, true); + + return $this; + } + + public function orNotHaving($field, $value) : self + { + $this->queryBuilder->having($field, $value, Query\Where::OPERATOR_NOT_EQUAL, Query\Where::CONDITION_OR, true); + + return $this; + } + + public function in($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + $this->queryBuilder->where($field, $value, $operator); + + return $this; + } + + public function orIn($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_OR); + + return $this; + } + + public function notIn($field, $value) : self + { + $this->queryBuilder->where($field, $value, Query\Where::OPERATOR_NOT_EQUAL); + + return $this; + } + + public function orNotIn($field, $value) : self + { + return $this->orNot($field, $value, Query\Where::OPERATOR_NOT_EQUAL, Query\Where::CONDITION_OR, true); + } + + public function like($field, $value) : self + { + $this->where($field, $value, Query\Where::OPERATOR_LIKE); + + return $this; + } + + public function orLike($field, $value) : self + { + $this->or($field, $value, Query\Where::OPERATOR_LIKE); + + return $this; + } + + public function notLike($field, $value) : self + { + $this->notWhere($field, $value, Query\Where::OPERATOR_LIKE); + + return $this; + } + + public function likes(array $fieldValues, string $condition = Query\Where::CONDITION_AND) : self + { + foreach($fieldValues as $field => $value) { + $this->like($field, $value); + } + + return $this; + } +} \ No newline at end of file