mailcow 中的 Adldap2 模型(Model)完全指南:LDAP 记录的创建、更新、删除与属性操作
mailcow 中的 Adldap2 模型Model完全指南LDAP 记录的创建、更新、删除与属性操作【免费下载链接】mailcow-dockerizedmailcow: dockerized - 项目地址: https://gitcode.com/GitHub_Trending/ma/mailcow-dockerized本篇文章以 mailcow-dockerized 仓库内置的 Adldap2 库官方文档 docs/models/model.md 为核心系统讲解 Adldap2 的模型层设计从make()工厂创建 LDAP 记录到save()/create()/update()的持久化差异再到多值属性的读写、移动重命名、删除与自定义模型扩展。读者学完后将能独立使用 Adldap2 完成目录中用户、组、OU 等对象的完整生命周期管理并能理解 mailcow 自身 LDAP 身份源同步脚本背后的实现原理。一、模型即记录Adldap2 的 ActiveRecord 模式Adldap2 实现了经典的 ActiveRecord 模式目录Directory中的每一条 LDAP 记录都对应一个独立的模型实例Model Instance。换言之一个Adldap\Models\User对象就是一个 LDAP 用户条目你操作模型就是在操作目录中的真实记录。从源码结构看这一设计体现在 src/Models/Model.php 的抽象基类Adldap\Models\Model中它实现了ArrayAccess与JsonSerializable接口并组合了HasEvents模型事件与HasAttributes属性系统两个 Trait。所有具体模型User、Group、Computer、Contact、Container、OrganizationalUnit等都继承自该基类因此拥有一致的属性访问、保存、删除能力。模型构造时接收两个关键依赖$attributes初始属性数组与$builder当前查询构建器用于回写目录。这也是后续所有增删改查操作能精准落盘的基础——每个模型都持有指向其所在目录的查询上下文。二、创建模型make()工厂方法手工拼接 LDAP 条目历来繁琐而 Adldap2 将其简化为一次工厂调用。当你持有 provider 实例时调用make()即可返回一个Adldap\Models\Factory实例$factory $provider-make();也可以直接链式调用一步到位$user $provider-make()-user();从 src/Models/Factory.php 的实现可以看出工厂方法的底层逻辑user()等方法会先从当前 Schema 中解析出对应的模型类名$this-schema-userModel()实例化模型后自动为其写入objectClass属性$this-schema-userObjectClasses()。也就是说你无需手工指定objectClass工厂会按 Active Directory 默认 Schema 补全这正是创建 LDAP 条目毫不费力的源码级原因。可用的 Make 方法所有 make 方法都接受一个可选的$attributes参数用于在创建时直接填充属性// Adldap\Models\User $user $provider-make()-user([ cn John Doe, ]); // Adldap\Models\Computer $computer $provider-make()-computer([ cn COMP-101, ]); // Adldap\Models\Contact $contact $provider-make()-contact([ cn Suzy Doe, ]); // Adldap\Models\Container $container $provider-make()-container([ cn VPN Users, ]); // Adldap\Models\Group $group $provider-make()-group([ cn Managers, ]); // Adldap\Models\OrganizationalUnit $ou $provider-make()-ou([ name Acme, ]);工厂方法涵盖的核心模型类User、Computer、Contact、Container、Group、OrganizationalUnit等在 src/Models/ 目录下均有对应实现例如 User.php 与 Group.php。其中ou()方法见 Factory.php会自动设置objectClass为top与organizationalUnit的组合保证 OU 条目符合目录 Schema 要求。三、保存模型save()与create()/update()的分工创建模型实例只发生在内存中要真正写入目录必须显式持久化。当你持有任意模型实例时调用save()即可将变更写入服务器它返回一个布尔值$user $provider-make()-user([ cn New User, ]); if ($user-save()) { // User was saved. } else { // There was an issue saving this user. }从 Model.php 的实现可以看到save()是一个智能分派方法$saved $this-exists ? $this-update($attributes) : $this-create($attributes);它依据模型的$exists标志自动决定走update()还是create()并在前后分别触发Saving/Saved模型事件。注意当模型保存成功无论是新建还是更新后模型的属性会在后台与 LDAP 服务器重新同步syncRaw()。这样你可以在同一请求中继续执行其他依赖该模型已存在的操作例如修改组成员关系或读取服务端生成的objectGUID。手动创建create()如果你确定模型在目录中尚不存在可以使用create()方法$user $provider-make()-user([ cn New User, ]); if ($user-create()) { // User was created. } else { // There was an issue creating this user. }注意调用create()时如果模型还没有 distinguished nameDNAdldap2 会自动为你生成一个——使用配置中的base_dn加上模型的 common namecn拼接而成。对应源码为 Model.php 中的getCreatableDn()$this-getDnBuilder()-addCn($this-getCommonName())。若生成出的 DN 与查询基 DN 相同即缺少有效 RDN则会抛出UnexpectedValueException。手动更新update()如果你确定模型已存在于目录中可以直接使用update()方法$user $provider-search()-whereEquals(cn, John Doe)-firstOrFail(); $user-displayName Suzy Doe; if ($user-update()) { // User was updated. } else { // There was an issue updating this user. }update()的内部流程见 Model.php值得细看它会先调用fill()合并传入属性再通过getModifications()计算脏属性并生成一批 BatchModification最终调用连接层的modifyBatch()一次性批量提交。提交成功后立即syncRaw()重新同步属性并清空 modifications。若没有产生任何修改update()也会返回true避免无修改即失败的误判。四、检查模型存在性exists属性判断一个模型是否真实存在于目录中使用exists属性。它的判定逻辑非常直观当模型由搜索结果构造时exists自动为true见 HasAttributes.php 中setRawAttributes()末尾的$this-exists true;。$user $provider-search()-find(jdoe); $user-exists; // Returns true. if ($user-delete()) { $user-exists; // Returns false. }而通过工厂新建的模型exists初始为false只有保存成功后才变为true$user $provider-make()-user([ cn John Doe, ]); $user-exists; // Returns false. if ($user-save()) { $user-exists; // Returns true. }五、属性系统一切皆是多值数组由于 LDAP 本质上是多值的multi-valuedAdldap2 中模型的所有属性都以数组形式存放。例如一个用户模型的getAttributes()输出可能如下var_dump($user-getAttributes()); // Returns: /* [ cn [ 0 John Doe, ], sn [ 0 Doe, ], givenname [ 0 John ], useraccountcontrol [ 0 512 ], mail [ 0 jdoeacme.org, 1 john-doeacme.org, ], memberof [ 0 cnAccountants,ouGroups,dcacme,dcorg, 1 cnEmployees,ouGroups,dcacme,dcorg, 2 cnUsers,ouGroups,dcacme,dcorg, ], ] */可以看到mail与memberof这类多值属性确实持有多个下标。所有模型都继承自基类Adldap\Models\Model因此每个模型都具备一整套便捷的属性检索方法。在源码层面setAttribute()见 HasAttributes.php会强制把标量值包成数组is_array($value) ? $value : [$value]保证所有模型方法都能以一致的方式处理属性同时会把dn自动规范化为完整的distinguishedName属性名并将所有键名小写化以规避大小写差异。5.1 获取属性获取属性有若干种方式// 返回用户全部属性的数组 $user-getAttributes(); // 返回用户全部邮箱地址的数组不存在时返回 null $user-getAttribute(mail); // 返回用户第一个邮箱地址不存在时返回 null $user-getAttribute(mail, 0); // 返回用户第一个邮箱地址不存在时返回 null $user-getFirstAttribute(mail); // 通过属性magic property获取全部邮箱地址 $user-mail; // 通过数组下标获取第一个邮箱地址 $user-mail[0];getAttribute($key, $subKey)的第二个参数支持指定子键其实现HasAttributes.php会先做键名归一化再决定返回整个数组还是单个子值。使用 Getter 方法部分属性提供了语义化的 Getter让你无需记忆底层 LDAP 属性名。例如获取用户邮箱$user-getEmail();所有模型通用的 Getter 方法以下方法在所有返回的模型上均可用// 返回模型的 name 属性 $model-getName(); // 返回模型的 cn 属性 $model-getCommonName(); // 返回模型的 displayname 属性 $model-getDisplayName(); // 返回模型的 samaccountname 属性 $model-getAccountName(); // 返回模型的 samaccounttype 属性 $model-getAccountType(); // 返回模型的 whencreated 属性 $model-getCreatedAt(); // 返回模型的 whencreated 属性MySQL 时间戳格式 $model-getCreatedAtDate(); // 返回模型的 whencreated 属性Unix 时间戳 $model-getCreatedAtTimestamp(); // 返回模型的 whenchanged 属性 $model-getUpdatedAt(); // 返回模型的 whenchanged 属性MySQL 时间戳格式 $model-getUpdatedAtDate(); // 返回模型的 whenchanged 属性Unix 时间戳 $model-getUpdatedAtTimestamp(); // 返回模型的 objectclass 属性 $model-getObjectClass(); // 返回模型的根对象类别字符串 $model-getObjectCategory(); // 返回模型的对象类别数组 $model-getObjectCategoryArray(); // 返回模型的对象类别 distinguished name $model-getObjectCategoryDn(); // 返回模型的 SID二进制 $model-getObjectSid(); // 返回模型的 GUID二进制 $model-getObjectGuid(); // 返回模型的 SID字符串 $model-getConvertedSid(); // 返回模型的 GUID字符串 $model-getConvertedGuid(); // 返回模型的主组 ID $model-getPrimaryGroupId(); // 返回模型的 instancetype 属性 $model-getInstanceType(); // 返回模型的 maxpwdage 属性 $model-getMaxPasswordAge();这些 Getter 在 Model.php 中逐一实现。值得注意的细节包括getConvertedGuid()/getConvertedSid()内部通过Adldap\Models\Attributes\Guid与Sid类把 LDAP 返回的二进制值转换为可读字符串Model.php解析失败时优雅返回null。getCreatedAtDate()使用dateFormat默认Y-m-d H:i:s适合 MySQL 时间戳格式化getCreatedAtTimestamp()则按timestampFormatYmdHis.0Z兼容 Active Directory解析为 Unix 时间戳见 HasAttributes.php。getMaxPasswordAgeDays()把maxpwdage的 100 纳秒单位换算为天数(int) (abs($age) / 10000000 / 60 / 60 / 24)。关于特定模型的更多 Getter 说明可查阅 docs/models/ 目录下各模型文档例如 user.md 与 group.md。获取脏已修改属性使用getDirty()可以获取模型中被修改过的属性$user $provider-search()-users()-find(john); // 返回 array [0 John Doe] var_dump($user-cn); $user-setAttribute(cn, Jane Doe); // 返回 array [cn [0 Jane Doe]] var_dump($user-getDirty()); // 属性已被修改 - 返回 array [0 Jane Doe] var_dump($user-cn);该方法返回的数组以被修改的属性名为键、以属性的新值数组为值。其实现HasAttributes.php会遍历当前属性与original快照逐一比对并通过array_values()重置数组下标——源码注释明确指出这是因为 LDAP requiring consecutive indices (0, 1, 2 etc.)即 LDAP 批量修改要求下标连续。获取原始未修改属性使用getOriginal()获取模型的原始属性快照$user $provider-search()-users()-find(john); // 返回 array [0 John Doe] var_dump($user-cn); $user-setAttribute(cn, Jane Doe); // 属性已被修改 - 返回 array [0 Jane Doe] var_dump($user-cn); // 获取原始值 - 返回 array [0 John Doe] var_dump($user-getOriginal()[cn]);注意当你save()一个模型后模型的原始属性会重新同步为新的属性集合syncOriginal()因此保存后getOriginal()反映的是最新状态。5.2 设置属性与读取类似设置属性同样有多种方式// 通过方法设置 $user-setAttribute(cn, John Doe); // 指定子键覆盖特定位置的属性值 $user-setAttribute(mail, other-mailmail.com, 0); // 设置第一个属性值 $user-setFirstAttribute(mail, jdoemail.com); // 通过属性直接设置 $user-cn John Doe; // 批量设置属性 $user-fill([ cn John Doe, mail jdoemail.com, ]);fill()HasAttributes.php本质上是循环调用setAttribute()因此批量填充与单个设置行为完全一致。设置布尔属性字符串陷阱设置布尔类型的属性值时不能使用0/1/true/false——这些值在保存时会被简单地转换成整数而 LDAP 服务器对某些属性会因此报错。你必须使用布尔值的字符串版本TRUE/FALSE才能被 LDAP 服务器正确接受$user-setFirstAttribute(msExchHideFromAddressLists, TRUE); $user-save();在基类中还存在convertStringToBool()辅助方法Model.php用于把 Schema 定义的字符串布尔值安全地转换为 PHP 布尔值。5.3 创建属性要创建一个模型上尚不存在的属性直接像普通属性一样设置即可$user $provider-search()-whereEquals(cn, John Doe)-firstOrFail(); $user-new New Attribute; $user-save();如果设置的属性原本不在模型上调用save()时它会自动被创建。如果你希望手动、单独地创建新属性使用createAttribute($attribute, $value)方法if ($user-createAttribute(new, New Attribute)) { // Attribute created. }createAttribute()的实现Model.php要求模型已存在$this-exists通过连接层的modAdd()执行 LDAP 添加操作成功后默认触发syncRaw()重新同步可通过第三参数$sync false关闭。5.4 更新属性修改属性既可以用 setter 方法也可以直接赋值注意如果模型原本没有该属性使用 setter 同样可以创建新属性。$user $provider-search()-whereEquals(cn, John Doe)-firstOrFail(); $user-cn New Name; // 或者使用 setter $user-setCommonName(New Name); $user-save();如果希望单独更新某个属性使用updateAttribute($attribute, $value)if ($user-updateAttribute(cn, New Name)) { // Successfully updated attribute. }updateAttribute()Model.php通过连接层的modReplace()执行 LDAP 替换操作同样在成功后默认syncRaw()。5.5 删除属性删除属性最简单的方式是把属性设为null$user-cn null; $user-save();也可以调用deleteAttribute($attribute)if ($user-deleteAttribute(cn)) { // Attribute has been deleted. }deleteAttribute()Model.php的签名支持字符串或数组两种输入传入字符串时视为删除整个属性内部转换为[$attribute []]传入数组时支持精细删除——例如[memberuid username]只删除该属性的特定值而[memberuid []]删除整个属性底层通过连接层的modDelete()执行。5.6 属性检查检查属性是否存在使用hasAttribute()判断模型是否包含某属性// 检查基础属性是否存在 if ($user-hasAttribute(mail)) { // This user contains an email address. } // 按子键检查子属性是否存在 if ($user-hasAttribute(mail, 1)) { // This user contains a second email address. }hasAttribute()的实现HasAttributes.php在指定子键时使用点号路径$key.$subKey配合Arr::has()判断。统计属性数量使用countAttributes()获取模型属性总数$count $user-countAttributes(); var_dump($count); // Returns int检查模型是否位于某个 OU 内使用inOu()判断模型是否位于指定 OU 之下if ($model-inOu(User Accounts)) { // This model is inside the User Accounts OU. }也可以直接传入一个 OU 模型实例$serviceAccounts $provider-search()-ous()-find(Service Accounts); if ($model-inOu($serviceAccounts)) { // This model is inside the Service Accounts OU. }inOu()的实现Model.php有两种路径传入 OU 模型时直接检查 OU 的 DN 是否出现在当前模型 DN 中strpos传入字符串时则通过getDnBuilder()-getComponents(ou)提取 DN 中的ou组件进行正则匹配且默认不区分大小写可通过第二个参数$strict true开启严格匹配。检查模型是否可写使用isWritable()判断模型是否可写if ($model-isWritable()) { // You can modify this model. }其判定依据Model.php是模型的instanceType属性是否等于4(int) $this-getInstanceType() 4即 Active Directory 中可写副本的实例类型。5.7 强制重新同步模型属性如果需要强制重新同步模型属性使用syncRaw()$user-syncRaw();注意该方法会重新查询 LDAP 服务器获取当前模型并同步其属性。仅在你通过 LDAP 连接手动创建 / 更新 / 删除属性时才推荐使用。从 Model.php 可以看到syncRaw()先通过fresh()按 DN 重新查询模型再用查询结果覆盖当前属性若模型已被删除查不到则返回false。六、移动与重命名模型移动move()要把用户从一个 DN / OU 移动到另一个使用move()方法注意move()实际上是rename()方法的别名见 Model.php它先拆解当前 DN、取出最左侧 RDN再转调rename()。// 新的父级 distinguished name $newParentDn OUNew Ou,DCcorp,DClocal; if ($user-move($newParentDn)) { // User was successfully moved to the new OU. }也可以传入一个模型作为新的父容器// 新的父级 OU $newParentOu $provider-search()-ous()-find(Accounting); if ($user-move($newParentOu)) { // User was successfully moved to the new OU. }如果希望在移动后保留旧的 RDN相对可辨识名在第二个参数传入false// 新的父级 distinguished name $newParentDn OUNew Ou,DCcorp,DClocal; if ($user-move($newParentDn, $deleteOldRdn false)) { // User was successfully moved to the new OU, // and their old RDN has been left in-tact. }重命名rename()要重命名用户的 DN在rename()方法中传入新的相对可辨识名即可$newRdn cnNew Name; if ($user-rename($newRdn)) { // User was successfully renamed. }rename()的完整实现Model.php展示了移动与重命名在底层是同一操作调用连接层的rename($dn, $rdn, $newParentDn, $deleteOldRdn)成功后通过setDn({$rdn},{$newParentDn})更新本地 DN 并syncRaw()。若传入的$newParentDn是模型实例会自动取其 DN。注意move()在模型缺少 RDN 时会抛出UnexpectedValueExceptionCurrent model does not contain an RDN to move.。七、删除模型删除模型只需调用delete()方法$user $provider-search()-whereEquals(cn, John Doe)-firstOrFail(); echo $user-exists; // Returns true. if ($user-delete()) { // Successfully deleted user. echo $user-exists; // Returns false. }delete()的实现Model.php有几个值得注意的行为若模型不存在$this-exists false或没有 DN会抛出ModelDoesNotExistException定义于 ModelDoesNotExistException.php。支持递归删除传入$recursive true时会先通过newQuery()-listing()-in($this-getDn())列出所有直接叶子节点并逐个递归删除再删除自身。删除成功后exists被置为false并触发Deleting/Deleted事件。八、扩展自定义模型v8.0.0 引入注意该特性在v8.0.0版本引入。要使用自己的模型类你需要先创建新的 Schema关于 Schema 的完整说明可参考 docs 目录下的相关文档再把 Schema 注入 provider 的构造过程。下面完整走一遍扩展流程。第一步创建要扩展 / 覆写的模型类注意你的自定义模型必须继承自某个已有的 Adldap2 模型因为这些类上定义了仅属于它们的专用方法与属性。namespace App\Ldap\Models; use Adldap\Models\User as Model; class User extends Model { public function getCommonName() { // Overriding model method. } }第二步创建自定义 Schema 并返回你的模型类名namespace App\Ldap\Schemas; use App\Ldap\Models\User; class LdapSchema extends ActiveDirectory { public function userModel() { return User::class; } }第三步创建 provider 时把 Schema 注入配置$config [ hosts [...], username admin, password Pssword, schema MyApp\LdapSchema::class, ]; $ad new Adldap($config); $provider $ad-connect(); // 如果 jdoe 存在将返回你的自定义模型。 $user $provider-search()-users()-find(jdoe);这一机制与 Factory.php 的user()实现环环相扣工厂通过$this-schema-userModel()获取模型类名并实例化。因此只要自定义 Schema 覆写了userModel()后续所有通过工厂或搜索创建的用户模型都会自动换成你的自定义类——这是 Adldap2 提供类型安全扩展点的核心设计。九、mailcow 中的实际应用场景mailcow-dockerized 将 Adldap2 作为 Composer 依赖内置于 data/web/inc/lib/vendor/adldap2/adldap2/其包声明见 composer.json并在 composer.lock 中锁定版本。在邮件系统层面mailcow 的 LDAP 身份源同步脚本 data/conf/phpfpm/crons/ldap-sync.php 展示了与本文相同的模型思维脚本通过identity_provider(init)建立连接见 functions.inc.php然后执行查询并分页遍历LDAP 用户对每个用户判断 mailbox 记录是否存在决定调用mailbox(add, ...)创建或mailbox(edit, ...)同步属性$ldap_query $iam_provider-query(); if (!empty($iam_settings[filter])) { $ldap_query $ldap_query-rawFilter($iam_settings[filter]); } $response $ldap_query-where($iam_settings[username_field], *) -where($iam_settings[attribute_field], *) -select([$iam_settings[username_field], $iam_settings[attribute_field], displayname]) -paginate($max);脚本中通过$user[$iam_settings[username_field]][0]取属性首个值的方式ldap-sync.php正是本文所述LDAP 属性皆为多值数组这一模型约定在实际生产代码中的直接体现而先查询、判断存在性、再创建或更新的流程也正是exists属性与create()/update()分工思想的业务化落地。理解 Adldap2 的模型层是阅读和定制 mailcow 身份同步逻辑的基础。十、小结Adldap2 的模型层把 LDAP 记录的完整生命周期封装为面向对象的 PHP API通过make()工厂快速构造、通过save()/create()/update()智能持久化、通过统一的多值属性系统读写数据、通过move()/rename()/delete()完成目录结构调整并通过 Schema 自定义模型实现类型安全扩展。配合 docs/models/model.md 及其他模型文档user.md、group.md 等你可以在任意使用 Adldap2 的 PHP 项目中快速上手 LDAP 对象管理。【免费下载链接】mailcow-dockerizedmailcow: dockerized - 项目地址: https://gitcode.com/GitHub_Trending/ma/mailcow-dockerized创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →