PHP也是一门在不断变化的语言。
本书介绍了PHP 5.3之后出现的一些新特性,对PSR标准做了介绍,但是因为年代(2015年出版)的问题,PSR标准建议到官网进行阅读,本书最后还对配置、部署方面提出了一些建议。
PHP也是一门在不断变化的语言。
本书介绍了PHP 5.3之后出现的一些新特性,对PSR标准做了介绍,但是因为年代(2015年出版)的问题,PSR标准建议到官网进行阅读,本书最后还对配置、部署方面提出了一些建议。
CodeIgniter3是一个相当轻量、简便的并且上手难度低的PHP应用开发框架。在CodeIgnitor2时代曾经接触并开发了一些项目。目前最新版本是3.1.5
。优点个人认为有:
同时,个人也认为以下功能还可以有所变化:
文章的剩余内容将会针对以上的各个方面详细说明。
PHP5/7加上7.19的libcurl,设置低于1s的超时时间时,curl_exec
仍会执行超过1s以上。原因在于此版本的libcurl实现逻辑上以1000ms作为curl_exec
中poll
系统调用的超时值。
共享内存指在多处理器的计算机系统中,可以被不同中CPU访问的大容量内存。是众多进程间通信(IPC)方式中最快的一种,无需进行数据拷贝等操作,即可在各个进程之间共享数据。
PHP同样也提供了对共享内存操作的可能性,通过shmop
扩展实现。
结合一个实际使用的场景,PHP daemon多进程从上游获取用户ID,需要与指定数据集去重,用户ID大约为12位的数字。要求是不能多去重(即不能存在误伤的判断),但是也不能少去重。
考虑到多进程处理时,需要考虑如何实现便捷,快速的进行判断用户ID已存在于数据集之中。在数据结构上可以使用的几种方法有:
下面分别说明三者的优缺点:
Bitmap(位图,以下不加区分的使用)的特点在于数据密度大时(即已排序的情况下,相邻数字间隔不大),是极为节省内存用量的数据结构。无论数字多大,在内存中只通过1 Bit
进行表示。假设用1 Byte
的空间进行数字的表示,数据集为[1,3,4,5,7],则这一组数据的位图可以表示为:
1 2 |
01011101 |
即便数字相当巨大,在已知最小值的情况下,同样通过同样大小的空间也可表示,如数据集为[100000001,100000003,100000004,100000005,100000007],同样也可以用同样一个位图进行表示,因为所有数字的都可以认为相对于100000000
进行了偏移操作。
Bitmap操作起来速度与便利性也相当令人满意,只需要根据数字大小,找到对应的位,判断当前位的0/1值即可,位运算操作的速度之快无需多言。
然而,Bitmap的最大问题在于,如果存储的是数值的顺序信息,那么整个Bitmap的数据才是最有效的。即,如果已经数字本身是有序的,如从1开始,一直到10000,或者是有办法迅速的知道位置是1的数字的具体字面值,那么存储的位会更加的高效。
在存储用户ID这一个场景时,Bitmap不一定适用,因为用户ID可能会长于10位,如果把用户ID当成数字来看,同时考虑到可能存在的一些业务形态(6,8之类的靓号逻辑,4之类的避讳逻辑),可能得到的Bitmap就相当的“稀疏”(0过多,1过少),造成的结果就是内存的有效使用率降低。假设用户ID最大值是9999999999,仅仅在10位数的用户ID的情况下,为了包含所有的数字,需要开辟约1192.09MB大小的内存空间,当然实际应用中很可能会比这个要小,通过找到偏移值(获取比较集的最大最小值,确定偏移值,减少无用0的内存占用)、数据分块(比如前5位相同的比率大,把数字前5位作为一个集合,只生成后5位的Bitmap提高表示有效程度)等方式,但是又会引入一些其他的问题,或者效果不佳(比较集合中存在1和9999999999两个用户ID)或者是难于管理(前5位有上万种组合
)。
如果在可以接受误伤的情况下,有一个更优的方案,即布隆过滤器(Bloom filter),这是通过Bitmap可以完成的,综合速度和存储压力都较优方案。
Hash(哈希表)的特点在于快,理论上来说,Hash的查询和写入时间复杂度都是O(1)。
编程语言如果提供了Hash的数据结构,那么应用数据结构就成为了一个看起来不错选择了。
实际实现中,Hash存在的第一个可能存在的问题是Hash冲突的解决,如果用开放地址法(Open addressing)进行解决,可能的问题在于冲突之后查询下一个可用地址的次数过多,而用链表(Separate chaining)解决,则会存在退化的情况(所有值都hash到一个链表中)。不过这些问题一般来说都应该是在应用过程中由编程语言关心的。
这次应用限定了编程语言为PHP,那么从PHP的实现上来看看应用Hash是否可行。
PHP里的数组就提供了Hash的功能,PHP数组的便利程度无需多言,在去重这一个场景上,完全可以通过用户ID作为key,写入布尔值作为value,通过isset()
方法快速的进行去重操作。
速度上我们可能不再担忧了,但是内存占用上呢?
目前PHP的最新版本为PHP 7.1.0
,常规编译安装后,通过如下脚本获取0~1000000在数组中的内存占用情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php ini_set('memory_limit', '512M'); $repeat = isset($argv[1]) ? $argv[1] : 0; echo 'memory usage(B):' . memory_get_usage() . "\n"; $a = []; for($i = 0; $i < $repeat; $i++) { $a[sprintf("%07d", $i)] = '1'; } echo 'memory usage(B):' . memory_get_usage() . "\n"; |
运行结果为:
1 2 3 4 |
$ /usr/local/php7/7.1.0/bin/php array_mem_usage.php 1000000 memory usage(B):350688 memory usage(B):358099536 |
仅仅1000000的7位字符串作为hash key的数据集,就需要耗费超过341MB的内存。可是即便是作为文本文件,这些数据集在通过换行符号分隔的情况下,完全加载到内存只需要大约8MB的内存使用。是什么造成了如此大的差距呢?
从PHP的源码来看(源码目录下的Zend/zend_types.h
),PHP数组通过HashTable
这一个struct实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
typedef struct _zend_array HashTable; struct _zend_array { zend_refcounted_h gc; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar flags, zend_uchar nApplyCount, zend_uchar nIteratorsCount, zend_uchar consistency) } v; uint32_t flags; } u; uint32_t nTableMask; Bucket *arData; uint32_t nNumUsed; uint32_t nNumOfElements; uint32_t nTableSize; uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor; }; |
数据的实际存储部分即Bucket
,对内存占用影响起到决定性作用的也正是Bucket
这个数据结构,Bucket的定义为:
1 2 3 4 5 6 |
typedef struct _Bucket { zval val; zend_ulong h; /* hash value (or numeric index) */ zend_string *key; /* string key or NULL for numerics */ } Bucket; |
在Bucket
中包含zval
,zend_ulong
,以及zend_string
三种数据结构,下面分别来看看这几个数据结构。
zval
的定义如下:
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 |
typedef struct _zval_struct zval; struct _zval_struct { zend_value value; /* value */ union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar type, /* active type */ zend_uchar type_flags, zend_uchar const_flags, zend_uchar reserved) /* call info for EX(This) */ } v; uint32_t type_info; } u1; union { uint32_t next; /* hash collision chain */ uint32_t cache_slot; /* literal cache slot */ uint32_t lineno; /* line number (for ast nodes) */ uint32_t num_args; /* arguments number for EX(This) */ uint32_t fe_pos; /* foreach position */ uint32_t fe_iter_idx; /* foreach iterator index */ uint32_t access_flags; /* class constant access flags */ uint32_t property_guard; /* single property guard */ } u2; }; |
zval
中又包含zend_vlaue
,那么通过zend_value
的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
typedef union _zend_value { zend_long lval; /* long value */ double dval; /* double value */ zend_refcounted *counted; zend_string *str; zend_array *arr; zend_object *obj; zend_resource *res; zend_reference *ref; zend_ast_ref *ast; zval *zv; void *ptr; zend_class_entry *ce; zend_function *func; struct { uint32_t w1; uint32_t w2; } ww; } zend_value; |
可以看出zend_value
作为一个union至少要占用8个字节(最大的内存占用来自于当中包含的zend_long
,在Zend/zend_long.h
中被定义为typedef int64_t zend_long;
),所以,作为一个struct,zval
会占用sizeof(value)+sizeof(u1)+sizeof(u2)=8+4+4=16 Bytes。
在Zend/zend_long.h
中被定义为typedef int64_t zend_ulong;
,所以会占用8 Bytes。
来看zend_string
的定义:
1 2 3 4 5 6 7 |
struct _zend_string { zend_refcounted_h gc; zend_ulong h; /* hash value */ size_t len; char val[1]; }; |
其中zend_refcounted_h
的定义为:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
typedef struct _zend_refcounted_h { uint32_t refcount; /* reference counter 32-bit */ union { struct { ZEND_ENDIAN_LOHI_3( zend_uchar type, zend_uchar flags, /* used for strings & objects */ uint16_t gc_info) /* keeps GC root number (or 0) and color */ } v; uint32_t type_info; } u; } zend_refcounted_h; |
可以看到zend_refcounted_h
的空间占用为sizeof(refcount)+sizeof(u)=4+4=8 Bytes。
size_t
这里需要注意,因为在64位OS上编译,这里会占用8 Bytes。
结构成员val用于存储key的值,由于key都是7位长的字符串,所以这里会占用7 Bytes。
所以,在这里,空间占用为8+8+8+7=31 Bytes。
综上,直接统计数据结构的大小已经能明显看到PHP提供Hash是相当占用内存的,出于这个方面的考虑,基本可以认定Hash不适合当前的应用场景。
可能有人会说,Array算是什么办法,但是对于特定情况,Array确实可以使用。
比如C语言的数组,是在内存中的一片连续空间,也就是说,实际上内存的占用就是数据个数乘以单个数据需要占用的空间。从这个角度来看,是没有太多的内存浪费的。
当然PHP的数组不能这么看,因为PHP的数组仍然有太多的冗余信息。
数组的“缺点”在于查找的耗时。线性查找的耗时几乎让人无法接受,那么如果数据可以是有序的,通过二分查找耗时将会大大降低。
1M的数据,只需要通过至多10次查找即可判断对应的值是否存在。
从数据特点和存储用量来考虑,同时考虑到查询速度,Array在这个场景下胜出。
PHP可以使用共享内存,作为最简单的IPC方式,并且使用方式相当简单,PHP的shmop
扩展中提供了对共享内存的操作能力。
共享内存在PHP的创建和打开工作是通过shmop_open
方法实现的。定义如下:
1 2 |
int shmop_open ( int $key , string $flags , int $mode , int $size ) |
PHP的共享内存的创建实际上是通过shmget
这一系统调用实现的,参见shmop
扩展源码:
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 |
PHP_FUNCTION(shmop_open) { zend_long key, mode, size; struct php_shmop *shmop; struct shmid_ds shm; char *flags; size_t flags_len; if (zend_parse_parameters(ZEND_NUM_ARGS(), "lsll", &key, &flags, &flags_len, &mode, &size) == FAILURE) { return; } // 其他代码 if (shmop->shmflg & IPC_CREAT && shmop->size < 1) { php_error_docref(NULL, E_WARNING, "Shared memory segment size must be greater than zero"); goto err; } shmop->shmid = shmget(shmop->key, shmop->size, shmop->shmflg); // 创建共享内存 if (shmop->shmid == -1) { php_error_docref(NULL, E_WARNING, "unable to attach or create shared memory segment '%s'", strerror(errno)); goto err; } // 其他代码 shmop->addr = shmat(shmop->shmid, 0, shmop->shmatflg); // 申请or打开的共享内存映射到当前进程的虚拟内存空间 if (shmop->addr == (char*) -1) { php_error_docref(NULL, E_WARNING, "unable to attach to shared memory segment '%s'", strerror(errno)); goto err; } RETURN_RES(zend_register_resource(shmop, shm_type)); err: efree(shmop); RETURN_FALSE; } |
shmget
返回的值是一个类似文件描述符的存在,因为它并不是一个真正的文件描述符,所以我们实际上的操作依据仅仅上是一个全局唯一的数字,用来表示共享内存。同时通过shmat
系统调用将共享内存映射到当前进程的地址虚拟空间之中。
共享内存打开方法中的第一个参数指定的key,是标识这一共享内存片段的依据,需要全局唯一,一个方法就是通过一个确实存在的文件,利用ftok
系统调用,生成一个全局唯一的key。文件名不是决定key值的决定因素,决定因素是文件的inode号。
1 2 |
void shmop_close ( resource $shmid ) |
关闭的实现的是通过shmdt
系统调用完成的。在扩展的MINIT
阶段,通过zend_register_list_destructors_ex
方法注册了资源析构方法为rsclean
,对共享内存的的ID看做是资源(Resource)。
1 2 3 4 5 6 7 |
PHP_MINIT_FUNCTION(shmop) { shm_type = zend_register_list_destructors_ex(rsclean, NULL, "shmop", module_number); return SUCCESS; } |
在调用这一方法时,通过资源删除APIzend_list_close
调用注册的rsclean
方法完成对资源的释放。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
PHP_FUNCTION(shmop_close) { zval *shmid; struct php_shmop *shmop; if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &shmid) == FAILURE) { return; } if ((shmop = (struct php_shmop *)zend_fetch_resource(Z_RES_P(shmid), "shmop", shm_type)) == NULL) { RETURN_FALSE; } zend_list_close(Z_RES_P(shmid)); } |
而rsclean
的实现如下:
1 2 3 4 5 6 7 8 |
static void rsclean(zend_resource *rsrc) { struct php_shmop *shmop = (struct php_shmop *)rsrc->ptr; shmdt(shmop->addr); efree(shmop); } |
对共享内存ID进行了shmdt
操作,同时释放了的申请共享内存操作结构体。
1 2 |
bool shmop_delete ( resource $shmid ) |
这一个方法中,是shmctl
系统调用的表现的时候了。删除一段共享内存只需要将系统调用中的第二个参数设定为IPC_RMID
即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
PHP_FUNCTION(shmop_delete) { zval *shmid; struct php_shmop *shmop; if (zend_parse_parameters(ZEND_NUM_ARGS(), "r", &shmid) == FAILURE) { return; } if ((shmop = (struct php_shmop *)zend_fetch_resource(Z_RES_P(shmid), "shmop", shm_type)) == NULL) { RETURN_FALSE; } if (shmctl(shmop->shmid, IPC_RMID, NULL)) { php_error_docref(NULL, E_WARNING, "can't mark segment for deletion (are you the owner?)"); RETURN_FALSE; } RETURN_TRUE; } |
当然,删除共享内存也可以通过Linux中的ipcrm命令完成,如ipcrm
的man中提到的,如果知到key(即创建/打开
部分提到的共享内存全局唯一的标识)则使用-M
参数,知道ID则使用-m
参数。
1 2 3 4 5 6 |
-M shmkey Mark the shared memory segment associated with key shmkey for removal. This marked segment will be destroyed after the last detach. -m shmid Mark the shared memory segment associated with id shmid for removal. This marked segment will be destroyed after the last detach. |
1 2 |
string shmop_read ( resource $shmid , int $start , int $count ) |
读取的方法则是通过共享内存ID以及开始位置以及读取的长度获得一个字符串。
从实现上来看,以下几种情况会返回false并打印WARNING日志:
1 2 |
int shmop_write ( resource $shmid , string $data , int $offset ) |
实现上来说,实际上是通过memcpy
将字符串的值复制到共享内存的指定位置。
近期接触 Laravel
这一框架之后,对使用便捷性和功能的丰富感到十分满意,同时开发者与相关的社区都相当活跃,这样的框架算是相当之理想的了。
了解了框架的使用之后,自然是希望能够找到这一框架构建的应用进行进一步的学习。CMS作为常见的系统,模式上个人理解起来比较容易,同时涉及面也比较多,同时还有一定的实用性,学习起来有价值。
使用 Laravel 的 CMS 不少,有本文的主角October CMS、Asgard CMS、Lavalite,但是后续的这些,无论从 GitHub star数目上,还是更新频率上,以及社区活跃度上,完全无法与 October CMS 相提并论。
于是决定简单了解一下October CMS(以下将会用October表示,二者不做区分)。希望通过开发一个表单部件的方式,开始二次开发的学习。
本文写作的前提是:
October宣传自身是一个简单、现代、以人为本、通用的、可拓展的、有趣的、可靠地、易学、节省时间的CMS。
对于OctoberCMS,对于一对多的关系,可以在model层进行处理。
然而,处于对体验优化的考虑,简单的关联记录的效果并不能满足。譬如需要完成一个相关文章的编辑表单,如果能够以card的形式展现相关文章的头图
,标题
,摘要
等内容,那么会更加便于后台人员的操作,提供更好的体验。
同时,可以通过这一个简单的表单插件,熟悉October CMS表单插件的二次开发。
总而言之,需求如下:
头图
,标题
,摘要
OctoberCMS的有一类组件,被称为Form
组件,为后台操作数据提供了极大的方便,通过配置各种Form插件的组合,可以对数据进行操作。
常规的管理界面的开发,一种模式是通过编写接口,提供修改对应数据的功能,并且前端开发需要构建对应的操作界面。对于CMS系统,这样无疑是一个低效的行为,使得管理平台开发也成为了开发工作的一大负担,提供更为简便的管理平台开发方式,对于减少开发工作量有很大意义。
OctoberCMS的表单部件通过yaml配置文件的形式,将数据库字段与前端编辑表单部件关联起来,为快速构建针对于数据的管理界面提供了可能。
针对于基本的字符串,时间选择器,富文本编辑器,单选/复选框,OctoberCMS也都默认提供了。
对于表单部件,功能上可以一言概之,即通过图形方式构建JSON格式的字符串,并写入数据库。
细化需求来说,关键点在于表单,数据格式,图形界面。
对于表单
,最简单的情况即通过一个input标签,在进行form提交时,带上需要提交的数据。
对于数据格式
,我们需要记录的是相关记录的数据库中的自增ID,存储格式会整编为JSON格式。
对于图形界面
,考虑到操作的便捷性,同时考虑到显示足够的有效信息,通过显示blog的头图、标题、摘要等信息的card的形式,表现关联记录。
对于表单来说,只需要确定表单中的name属性即可。
1 2 3 4 5 6 7 8 9 10 11 |
<input type="text" name="<?php echo $name ?>" id="<?php echo $this->getId() ?>" value="<?php echo json_encode($value); ?>" placeholder="" class="form-control" autocomplete="off" style="display: none;" > |
新增的情况,无需填充记录的默认值,而在更新的情况,则需要进行填充,相关PHP代码的说明会在后续提及。
选用JSON作为数据格式,记录各个关联记录的自增ID。
同时显示头图,标题,摘要,通过card形式展现。
操作上,需要有增加
,删除
,调整顺序
三种。
增加
操作可以通过弹出记录列表的形式,选取后展现。删除
则直接通过点击card上的删除按钮/文字即可。调整顺序
通过前移/后移按钮完成。OctoberCMS的表单插件需要符合一定的目录结构规则。
可以通过直接创建对应的目录以及文件,也可以通过内置的工具进行初始化:
1 2 |
php artisan create:formwidget rainlab.blog RelatedRecords |
rainlab.blog
是将要创建这一组件的插件的名称,这里可以换成自己插件的名字。关于插件,参见OctoberCMS的手册。
自动建立的目录结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
→ tree . └── formwidgets ├── RelatedRecords.php └── relatedrecords ├── assets │ ├── css │ │ └── relatedrecords.css │ └── js │ └── relatedrecords.js └── partials └── _relatedrecords.htm |
文件不多,简而言之,RelatedRecords.php
用来描述表单部件以及编写处理逻辑,relatedrecords.css
自然是控制表单后台操作样式,relatedrecords.js
则是实现表单项目在后台的前端操作逻辑,最后partials
中的_relatedrecords.htm
则定义了表单部件在后台的html展示部分。
如果不想通过内置工具生成表单部件,同样也可以直接在对应plugin的目录中中的widgets
子目录中直接新增插件的方式完成,仿照October默认的modules/backend中表单插件的结构即可:
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 |
→ tree widgets widgets ├── Filter.php ├── Form.php ├── Lists.php ├── ReportContainer.php ├── Search.php ├── Table.php ├── Toolbar.php ├── filter │ └── partials │ ├── _filter.htm ... │ └── _scope_switch.htm ├── form │ ├── assets │ │ └── js │ │ └── october.form.js │ └── partials │ ├── _field-container.htm ... │ └── _section.htm ├── lists │ ├── assets │ │ └── js │ │ └── october.list.js │ └── partials │ ├── _list-container.htm ... │ └── _setup_form.htm ├── reportcontainer │ ├── assets │ │ ├── css │ │ │ └── reportcontainer.css │ │ ├── js │ │ │ └── reportcontainer.js │ │ ├── less │ │ │ └── reportcontainer.less │ │ └── vendor │ │ └── isotope │ │ ├── jquery.isotope.js │ │ └── jquery.isotope.min.js │ └── partials │ ├── _container.htm ... │ └── _widget_toolbar.htm ├── search │ └── partials │ └── _search.htm ├── table │ ├── ClientMemoryDataSource.php │ ├── DataSourceBase.php │ ├── README.md │ ├── ServerEventDataSource.php │ ├── assets │ │ ├── css │ │ │ └── table.css │ │ ├── images │ │ │ └── table-icons.gif │ │ ├── js │ │ │ ├── build-min.js ... │ │ │ └── table.validator.required.js │ │ └── less │ │ └── table.less │ └── partials │ └── _table.htm └── toolbar └── partials └── _toolbar.htm |
本文的demo即是通过这种方式生成的,结构如下:
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 |
→ tree widgets ├── RelatedRecords.php └── relatedrecords ├── assets │ ├── css │ │ └── relatedrecords.css │ ├── fonts │ ├── img │ │ └── default.png │ ├── js │ │ └── relatedrecords.js │ └── libs │ └── materialize │ ├── LICENSE │ ├── README.md │ ├── css │ │ ├── materialize.css │ │ └── materialize.min.css │ ├── fonts │ │ └── roboto │ │ ├── Roboto-Bold.eot ... │ │ └── Roboto-Thin.woff2 │ └── js │ ├── materialize.js │ └── materialize.min.js └── partials └── _field_relatedrecords.htm |
可以看到,二者并无太大差别,都需要有css/js/partial以及主要的逻辑所在的.php文件,后续以demo中的文件组织结构进行描述。
这一文件要直接放置于widgets
的目录之下。
需要继承Backend\Classes\FormWidgetBase
这一基类,同时我们需要实现一些关键的方法,以便完成逻辑以及正确显示组件。
下面,将会说明几个关键的方法。
init()
方法顾名思义,即初始化表单部件,一些组件自身需要进行的变量定义,参数定义等操作可以在这里编写。
在使用过程中,每个表单部件都或多或少有一些自定义的参数,譬如富文本编辑器的大小等,这些参数通过yaml文件配置,但是如何才能在表单部件中的读取到呢?
可以在init()
中调用fillFromConfig()
这一成员方法,通过数组的形式,将参数名传入,之后将会出现同名的成员变量,其值就是传入的参数名称。如果没有设定,那么也可以指定默认值。
为了完成需求,我们需要允许配置:
titleField
card的标题域数据库字段名
imageField
card的图片域数据库字段名
contentField
card的正文域数据库字段名
modelClass
需要操作的Model名称
whereClause
查询数据时的WHERE子句内容
recordsPerPage
弹出列表页每页显示个数
在实际使用中,在models/yourmodel/
下的fields.yaml
文件中指定使用这一表单部件时,可以通过指定这些参数的形式,影响展现以及效果,形如:
1 2 3 4 5 6 7 8 9 10 |
related_samples: tab: sample_tab type: relatedrecords titleField: title imageField: img contentField: content recordsPerPage: 10 modelClass: Your\Plugin\Models\Sample whereClause: 'published = true' |
render()
方法,即完成组件的渲染,如果已经设定好了模板中需要的值,那么只需要通过makePartial
这一个成员方法即可对当前组件渲染完成。
实现以上两个方法之后,几乎这一组件就能基本上完成数据获取并展现的功能了。其他方法,也可以按照默认生成的文件的模式进行组织。
模板文件作为一个partial,文件名需要以下划线开头,放置在组件的partials
目录下。
由于实际要修改的数值是一个JSON格式的数组,提交时,实际上是作为表单的一对键值。为了完成表单提交,需要设定input标签的name属性。
那么问题来了,如何才能设定正确的name属性呢?
OctoberCMS框架在实现表单部件时,会根据表单部件所对应的Model和列的名称,生成key的名字,即input标签的name属性,可以通过成员变量formField
的方法getName()
获取。
在主逻辑文件RelatedRecords.php
的render()
(或者其他被这一方法调用的方法之中),为name属性赋值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public function prepareData() { $this->vars['value'] = $this->getLoadValue(); ... $this->vars['name'] = $this->formField->getName(); ... $this->vars['recordId'] = isset($this->model->id) ? $this->model->id : 0; } public function render() { $this->addCss($this->widgetDir . '/assets/css/relatedrecords.css'); $this->addJs($this->widgetDir . '/assets/js/relatedrecords.js'); $this->prepareData(); return $this->makePartial('field_relatedrecords'); } |
在partial中进行相应变量赋值代码编写即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<div class="input-group"> <input type="text" name="<?php echo $name ?>" id="<?php echo $this->getId() ?>" value="<?php echo json_encode($value); ?>" placeholder="" class="form-control" autocomplete="off" style="display: none;" > </div> |
而展现上,使用了部分materialize的样式,实现了图文card的显示效果。
图文card的底部,有添加记录的按钮,但是如何才能最便捷的展现出可以加入的记录呢?
这里可以借用OctoberCMS内置的一个特性:Remote popups。
想要通过列表展示数据对于OctoberCMS来说简直是轻而易举。从操作步骤上来看,可以描述如下:
添加
Ajax请求如何能够返回并显示html呢?作为OctoberCMS的一个重要特性,简要来说即通过特定的请求方法发出请求,指定操作的handler。这里可以通过js完成。
参见下的assets/js/relatedrecords.js
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var fetchRelatedRecords = function() { var $this = $(this); ... $(this).popup({ handler: 'onLoadRelatedRecords', extraData: { page: page, excludeIds: excludeIds, recordsPerPage: CONFIG.recordsPerPage, modelClass: CONFIG.modelClass, whereClause: CONFIG.whereClause, titleField: CONFIG.titleField, imageField: CONFIG.imageField, contentField: CONFIG.contentField } }); }; |
通过popup方法,指定了handler为onLoadRelatedRecords
方法。
handler方法归属的Controller,即当前编辑对象所对应的
Controller,所以,需要在Controller中实现对应的同名方法。demo中编辑的是Sample这一个对象,那么方法实现起来将会类似:
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 |
class Sample extends Controller { public function onLoadRelatedRecords() { $page = Request::input('page'); $recordsPerPage = Request::input('recordsPerPage'); $excludeIds = Request::input('excludeIds'); $modelClass = Request::input('modelClass'); $whereClause = Request::input('whereClause'); if (!class_exists($modelClass)) { echo "{$modelClass} not exists"; } /** * var $model | Model; */ $model = new $modelClass(); if (!is_array($excludeIds)) { $excludeIds = json_decode($excludeIds); } $records = $model->whereNotIn('id', $excludeIds); if ($whereClause) { $records = $records->whereRaw($whereClause); } $records = $records->orderBy('id', 'desc')->paginate($recordsPerPage, $page); return [ 'result' => $this->makePartial('list_related_records', [ 'records' => $records, 'modelClass' => $modelClass ]) ]; } } |
考虑到不能重复加入关联记录,所以每次请求要通过excludeIds
传入已加入的id,进行排除。
通过popup方式弹出列表之后,选择加入这些操作就相当容易实现了。
其他如前端的操作、样式的编写等等不在赘述。
完成上述工作之后,还需要关键的一步,即在Plugin.php
中的registerFormWidgets()
方法注册编写完成的组件。
1 2 3 4 5 6 7 8 9 10 11 12 |
public function registerFormWidgets() { // register the widget // relatedrecords in fields.yaml will represent this form widget return [ 'Your\Plugin\Widgets\RelatedRecords' => [ 'label' => 'Related Records', 'code' => 'relatedrecords' ] ]; } |
通过弹出的Modal窗口添加关联:
选择后可供调整顺序或移除: