[ad_1]
Trong phần đầu tiên của loạt bài này, chúng tôi đã học các khái niệm chính để xây dựng một ứng dụng theo thuyết bất khả thi nhất có thể. Trong phần thứ hai và cuối cùng này, chúng tôi sẽ tiến hành trừu tượng hóa một ứng dụng WordPress, làm cho mã của nó sẵn sàng để được sử dụng với các thành phần Symfony, khung công tác Laravel và CMS tháng 10 (dựa trên Laravel).
Truy cập dịch vụ
Trước khi chúng tôi bắt đầu trừu tượng hóa mã, chúng tôi cần cung cấp lớp tiêm phụ thuộc cho ứng dụng. Như được mô tả trong phần đầu tiên của loạt bài này, lớp này được thỏa mãn thông qua thành phần Symfony phụ thuộc DependencyInjection. Để truy cập các dịch vụ đã xác định, chúng tôi tạo một lớp ContainerBuilderFactory
chỉ lưu trữ một thể hiện tĩnh của đối tượng ContainerBuilder
:
sử dụng Symfony Element Depends ContainerBuilder;
lớp ContainerBuilderFactory {
$ tĩnh riêng;
Hàm tĩnh công khai init ()
{
self :: $ dụ = new ContainerBuilder ();
}
Hàm tĩnh công khai getInstance ()
{
tự trả về :: $ dụ;
}
}
Sau đó, để truy cập vào một dịch vụ có tên "cache"
ứng dụng yêu cầu như thế này:
$ cacheService = ContainerBuilderFactory :: getInstance () -> get ('bộ đệm');
// Làm gì đó với dịch vụ
// $ cacheService -> ...
Tóm tắt mã WordPress
Chúng tôi đã xác định các đoạn mã và khái niệm sau từ ứng dụng WordPress cần được trừu tượng hóa khỏi ứng dụng WordPress Ý kiến của WordPress:
- chức năng truy cập
- tên hàm
- tham số chức năng
- trạng thái (và các giá trị không đổi khác)
- chức năng của trình trợ giúp
- quyền người dùng
- tùy chọn ứng dụng
- tên cột cơ sở dữ liệu
- lỗi
- móc
- thuộc tính đối tượng
- trạng thái toàn cầu
- mô hình thực thể (meta, loại bài đăng, trang là bài đăng và phân loại bí mật và các thể loại -)
- dịch
- phương tiện truyền thông.
]
Chúng ta hãy tiến hành trừu tượng hóa chúng, từng cái một.
Lưu ý: Để dễ đọc, tôi đã bỏ qua việc thêm không gian tên vào tất cả các lớp và giao diện trong suốt bài viết này. Tuy nhiên, việc thêm các không gian tên, như được chỉ định trong Khuyến nghị Tiêu chuẩn PHP PSR-4, là điều bắt buộc! Trong số các ưu điểm khác, ứng dụng sau đó có thể được hưởng lợi từ việc tự động tải và tiêm phụ thuộc của Symfony có thể dựa vào tải dịch vụ tự động để giảm cấu hình của nó xuống mức tối thiểu.
Truy cập các chức năng
Mã thần chú đối với các giao diện, không triển khai, có nghĩa là tất cả các chức năng do CMS cung cấp không thể truy cập trực tiếp được nữa. Thay vào đó, chúng ta phải truy cập chức năng từ một hợp đồng (giao diện), trên đó chức năng CMS sẽ chỉ đơn giản là thực hiện. Khi kết thúc sự trừu tượng hóa, vì không còn mã WordPress nào được tham chiếu trực tiếp nữa, nên chúng tôi có thể trao đổi WordPress với một CMS khác.
Ví dụ, nếu ứng dụng của chúng tôi truy cập chức năng get_posts
:
$ post = get_posts ($ args);
Sau đó chúng ta phải trừu tượng hóa chức năng này theo một số hợp đồng:
giao diện PostAPIInter
{
chức năng công khai getPosts ($ args);
}
Hợp đồng phải được triển khai cho WordPress:
lớp WPPostAPI thực hiện PostAPIInterface
{
chức năng công khai getPosts ($ args) {
trả về get_posts ($ args);
}
}
Một dịch vụ "post_api"
phải được thêm vào tệp cấu hình tiêm phụ thuộc services.yaml
cho biết lớp nào giải quyết dịch vụ:
dịch vụ:
bài viết_api:
class: WPPostAPI
Và cuối cùng, ứng dụng có thể tham chiếu chức năng thông qua dịch vụ "post_api"
:
$ postAPISF :: getInstance () -> get ('post_api');
$ post = $ postAPIService-> getPosts ($ args);
Tên hàm
Nếu bạn đã nhận thấy từ mã được trình bày ở trên, hàm get_posts ] được tóm tắt là
getPosts
. Có một vài lý do tại sao đây là một ý tưởng hay:
- Bằng cách gọi hàm khác nhau, nó giúp xác định mã nào thuộc về WordPress và mã nào thuộc về ứng dụng trừu tượng của chúng tôi.
[19459027TênhàmphảiđượccamelCaseđểtuânthủPSR-2cốgắngxácđịnhmộttiêuchuẩnđểviếtmãPHP
Một số chức năng có thể được xác định lại, có ý nghĩa hơn trong bối cảnh trừu tượng. Chẳng hạn, hàm WordPress get_user_by ($ field, $ value)
sử dụng tham số $ field
với các giá trị "id"
"ID"
, "sên"
"email"
hoặc "đăng nhập"
để biết cách lấy người dùng. Thay vì sao chép phương pháp này, chúng ta có thể định nghĩa rõ ràng một chức năng riêng cho từng người trong số họ:
giao diện UsersAPIInterface
{
hàm công khai getUserById ($ value);
chức năng công khai getUserByEmail ($ value);
hàm công khai getUserBySlug ($ value);
chức năng công khai getUserByLogin ($ value);
}
Và những điều này được giải quyết cho WordPress:
lớp WPUsersAPI triển khai UsersAPIInterface
{
hàm công khai getUserById ($ value)
{
trả về get_user_by ('id', $ value);
}
chức năng công khai getUserByEmail ($ value)
{
trả về get_user_by ('email', $ value);
}
Hàm công khai getUserBySlug ($ value)
{
return get_user_by ('sên', $ value);
}
chức năng công khai getUserByLogin ($ value)
{
return get_user_by ('đăng nhập', $ value);
}
}
Một số chức năng khác nên được đổi tên vì tên của chúng truyền đạt thông tin về việc triển khai của chúng, có thể không áp dụng cho một CMS khác. Chẳng hạn, hàm WordPress get_the_ Author_meta
có thể nhận tham số "user_lastname"
chỉ ra rằng họ của người dùng được lưu trữ dưới dạng giá trị meta meta (được xác định là thuộc tính bổ sung cho một đối tượng, ban đầu không được ánh xạ trong mô hình cơ sở dữ liệu). Tuy nhiên, các CMS khác có thể có một cột "họ"
trong bảng người dùng, do đó, nó không áp dụng như một giá trị meta. (Định nghĩa thực tế của giá trị meta meta thực sự không nhất quán trong WordPress: function get_the_ Author_meta
cũng chấp nhận giá trị "user_email"
mặc dù email được lưu trữ trên bảng người dùng. 'thà tuân theo định nghĩa của tôi về giá trị của meta meta, và loại bỏ tất cả sự không nhất quán khỏi mã trừu tượng.)
Sau đó, hợp đồng của chúng tôi sẽ thực hiện các chức năng sau:
giao diện UsersAPIInterface
{
chức năng công khai getUserDisplayName ($ user_id);
chức năng công khai getUserEmail ($ user_id);
chức năng công khai getUserFirstname ($ user_id);
chức năng công khai getUserLastname ($ user_id);
...
}
Đã được giải quyết cho WordPress:
lớp WPUsersAPI triển khai UsersAPIInterface
{
chức năng công khai getUserDisplayName ($ user_id)
{
return get_the_ Author_meta ('display_name', $ user_id);
}
chức năng công khai getUserEmail ($ user_id)
{
return get_the_ Author_meta ('user_email', $ user_id);
}
chức năng công khai getUserFirstname ($ user_id)
{
return get_the_ Author_meta ('user_firstname', $ user_id);
}
chức năng công khai getUserLastname ($ user_id)
{
return get_the_ Author_meta ('user_lastname', $ user_id);
}
...
}
Các chức năng của chúng tôi cũng có thể được định nghĩa lại để xóa các giới hạn khỏi WordPress. Chẳng hạn, hàm update_user_meta ($ user_id, $ meta_key, $ meta_value)
có thể nhận được một thuộc tính meta tại một thời điểm, điều này có ý nghĩa vì mỗi trong số này được cập nhật trên truy vấn cơ sở dữ liệu riêng của nó. Tuy nhiên, CMS tháng 10 ánh xạ tất cả các thuộc tính meta với nhau trên một cột cơ sở dữ liệu, do đó, việc cập nhật tất cả các giá trị cùng nhau trên một hoạt động cơ sở dữ liệu sẽ hợp lý hơn. Sau đó, hợp đồng của chúng tôi có thể bao gồm một hoạt động updateUserMetaAttribut ($ user_id, $ meta)
có thể cập nhật một số giá trị meta cùng một lúc:
giao diện UserMetaInterface
{
cập nhật chức năng công cộngUserMetaAttribut ($ user_id, $ meta);
}
Điều này được giải quyết cho WordPress như thế này:
lớp WPUsersAPI triển khai UsersAPIInterface
{
Cập nhật chức năng công cộngUserMetaAttribut ($ user_id, $ meta)
{
foreach ($ meta dưới dạng $ meta_key => $ meta_value) {
update_user_meta ($ user_id, $ meta_key, $ meta_value);
}
}
}
Cuối cùng, chúng tôi có thể muốn xác định lại một chức năng để loại bỏ sự mơ hồ của nó. Chẳng hạn, hàm WordPress add_query_arg
có thể nhận các tham số theo hai cách khác nhau:
- Sử dụng một khóa và giá trị duy nhất:
add_query_arg ('key', 'value', ' http://example.com ');
- Sử dụng một mảng kết hợp:
add_query_arg (['key1' => 'value1', 'key2' => 'value2'],' http://example.com ');
Điều này trở nên khó khăn để giữ sự nhất quán trên các CMS. Do đó, hợp đồng của chúng tôi có thể định nghĩa các hàm addQueryArg
(số ít) và addQueryArss
(số nhiều) để loại bỏ sự mơ hồ:
chuỗi $ key, chuỗi $ value, chuỗi $ url);
Hàm công khai addQueryArss (mảng $ key_values, chuỗi $ url);
Tham số chức năng
Chúng ta cũng phải trừu tượng hóa các tham số cho hàm, đảm bảo chúng có ý nghĩa trong một bối cảnh chung. Để mỗi chức năng được trừu tượng hóa, chúng ta phải xem xét:
- đổi tên và / hoặc xác định lại các tham số;
- đổi tên và / hoặc xác định lại các thuộc tính được truyền trên các tham số mảng.
Chẳng hạn, hàm WordPress get_posts
nhận được một tham số duy nhất $ args
là một mảng các thuộc tính. Một trong những thuộc tính của nó là các trường
khi được đưa ra giá trị "id"
làm cho hàm trả về một mảng ID thay vì một mảng các đối tượng. Tuy nhiên, tôi cho rằng việc triển khai này quá cụ thể đối với WordPress và đối với bối cảnh chung, tôi thích một giải pháp khác: Truyền tải thông tin này thông qua một tham số riêng gọi là $ tùy chọn
dưới thuộc tính "kiểu trả về "
.
Để thực hiện điều này, chúng tôi thêm tham số $ tùy chọn
vào chức năng trong hợp đồng của chúng tôi:
giao diện PostAPIInterface
{
chức năng công khai getPosts ($ args, $ tùy chọn = []);
}
Thay vì tham chiếu giá trị không đổi WordPress "ids"
(mà chúng tôi không thể đảm bảo sẽ là giá trị được sử dụng trong tất cả các CMS khác), chúng tôi tạo giá trị không đổi tương ứng cho ứng dụng trừu tượng của chúng tôi:
Hằng số lớp
{
const RETURNTYPE_IDS = 'ids';
}
Việc triển khai WordPress phải ánh xạ và tạo lại các tham số giữa hợp đồng và việc thực hiện:
lớp WPPostAPI thực hiện PostAPIInterface
{
chức năng công khai getPosts ($ args, $ tùy chọn = []) {
if ($ tùy chọn ['return-type'] == Hằng số :: RETURNTYPE_IDS) {
$ args ['fields'] = 'id';
}
trả về get_posts ($ args);
}
}
Và cuối cùng, chúng tôi có thể thực thi mã thông qua hợp đồng của mình:
$ Options = [
'return-type' => Constants::RETURNTYPE_IDS,
];
$ post_ids = $ postAPIService-> getPosts ($ args, $ tùy chọn);
Trong khi trừu tượng hóa các tham số, chúng ta nên tránh chuyển nợ kỹ thuật WordPress của mình sang mã trừu tượng, bất cứ khi nào có thể. Chẳng hạn, tham số $ args
từ hàm get_posts
có thể chứa thuộc tính 'post_type'
. Tên thuộc tính này có phần gây hiểu nhầm, vì nó có thể nhận được một yếu tố ( post_type => "post"
) nhưng cũng là một danh sách của chúng ( post_type => "post, event"
), vì vậy tên này phải ở dạng số nhiều thay vào đó: post_types
. Khi trừu tượng hóa đoạn mã này, chúng ta có thể đặt giao diện của mình thành thuộc tính mong đợi post_types
thay vào đó, nó sẽ được ánh xạ tới post_type
.
với các tên khác nhau, mặc dù chúng có cùng mục tiêu, vì vậy tên của chúng có thể được thống nhất. Chẳng hạn, thông qua tham số $ args
chức năng WordPress get_posts
chấp nhận thuộc tính post_per_page
và chức năng get_users
]. Các tên thuộc tính này hoàn toàn có thể được thay thế bằng tên thuộc tính chung hơn giới hạn
.
Cũng nên đổi tên các tham số để dễ hiểu cái nào thuộc về WordPress và cái nào thuộc về WordPress đã được trừu tượng hóa. Chẳng hạn, chúng ta có thể quyết định thay thế tất cả "_"
bằng "-"
do đó, đối số mới được xác định của chúng tôi post_types
trở thành .
Áp dụng những cân nhắc trước đó, mã trừu tượng của chúng tôi sẽ như thế này:
lớp WPPostAPI thực hiện PostAPIInterface
{
chức năng công khai getPosts ($ args, $ tùy chọn = []) {
...
if (isset ($ args ['post-types'])) {
$ args ['post_type'] = $ args ['post-types'];
bỏ đặt ($ args ['post-types']);
}
if (isset ($ args ['limit'])) {
$ args ['posts_per_page'] = $ args ['limit'];
bỏ đặt ($ args ['limit']);
}
trả về get_posts ($ args);
}
}
Chúng tôi cũng có thể định nghĩa lại các thuộc tính để sửa đổi hình dạng của các giá trị của chúng. Chẳng hạn, tham số WordPress $ args
trong chức năng get_posts
có thể nhận thuộc tính date_query
có thuộc tính ( "sau"
" bao gồm "
v.v.) có thể được coi là cụ thể đối với WordPress:
$ date = current_time ('dấu thời gian');
$ args ['date_query'] = mảng (
mảng(
'sau' => ngày ('Y-m-d H: i: s', $ date),
'bao gồm' => đúng,
)
);
Để thống nhất hình dạng của giá trị này thành một cái gì đó chung chung hơn, chúng ta có thể thực hiện lại bằng cách sử dụng các đối số khác, chẳng hạn như "date-from"
và "ngày từ bao gồm"
(giải pháp này không thuyết phục 100%, vì nó dài dòng hơn so với WordPress):
lớp WPPostAPI thực hiện PostAPIInterface
{
chức năng công khai getPosts ($ args, $ tùy chọn = []) {
...
if (isset ($ args ['date-from'])) {
$ args ['date_args'] [] = [
'sau' => $ args ['date-from'],
'bao gồm' => sai,
];
bỏ đặt ($ args ['date-from']);
}
if (isset ($ args ['date-from-inclusive'])) {
$ args ['date_args'] [] = [
'sau' => $ args ['date-from-inclusive'],
'bao gồm' => đúng,
];
bỏ đặt ($ args ['date-from-inclusive']);
}
trả về get_posts ($ args);
}
}
Ngoài ra, chúng ta cần xem xét liệu có nên trừu tượng hóa hay không những tham số quá cụ thể đối với WordPress. Chẳng hạn, hàm get_posts
cho phép sắp xếp các bài đăng theo thuộc tính menu_order
mà tôi không nghĩ rằng nó hoạt động trong bối cảnh chung. Sau đó, tôi không muốn trừu tượng mã này và giữ nó trên gói dành riêng cho CMS cho
Cuối cùng, chúng tôi cũng có thể thêm các loại đối số (và, vì ở đây chúng tôi cũng trả lại các loại) cho chúng tôi hợp đồng hợp đồng, làm cho nó dễ hiểu hơn và cho phép mã bị lỗi trong thời gian biên dịch thay vì trong thời gian chạy:
giao diện PostAPIInterface
{
Hàm công khai getPosts (mảng $ args, mảng $ tùy chọn = []): mảng;
}
Các quốc gia (và các giá trị không đổi khác)
Chúng ta cần đảm bảo rằng tất cả các quốc gia đều có cùng một ý nghĩa trong tất cả các CMS. Chẳng hạn, các bài đăng trong WordPress có thể có một trong các trạng thái sau: "xuất bản"
"cấp phát"
"bản nháp"
hoặc "thùng rác"
. Để đảm bảo rằng ứng dụng tham chiếu phiên bản trừu tượng của các trạng thái chứ không phải trạng thái dành riêng cho CMS, chúng ta chỉ cần định nghĩa một giá trị không đổi cho mỗi trạng thái:
lớp PostStates {
const PUBOUNDED = 'xuất bản';
const PENDING = 'đang chờ xử lý';
const DRAFT = 'bản nháp';
const TRASH = 'thùng rác';
}
Như có thể thấy, các giá trị hằng số thực tế có thể hoặc không giống như trong WordPress: trong khi "xuất bản"
được đổi tên thành "được xuất bản"
những cái khác vẫn giữ nguyên.
Để triển khai cho WordPress, chúng tôi chuyển đổi từ giá trị bất khả tri sang giá trị cụ thể của WordPress:
lớp WPPostAPI triển khai PostAPIInter
{
chức năng công khai getPosts ($ args, $ tùy chọn = []) {
...
if (isset ($ args ['post-status'])) {
$ chuyển đổi = [
PostStates::PUBLISHED => 'publish',
PostStates::PENDING => 'pending',
PostStates::DRAFT => 'draft',
PostStates::TRASH => 'trash',
];
$ args ['post_status'] = $ convert [$args['post-status']];
bỏ đặt ($ args ['post-status']);
}
trả về get_posts ($ args);
}
}
Cuối cùng, chúng ta có thể tham chiếu các hằng số này trong suốt ứng dụng bất khả tri CMS của mình:
$ args = [
'post-status' => PostStates::PUBLISHED,
];
$ post = $ postAPIService-> getPosts ($ args);
Chiến lược này hoạt động theo giả định rằng tất cả các CMS sẽ hỗ trợ các trạng thái này. Nếu bất kỳ CMS nào không hỗ trợ một trạng thái cụ thể (ví dụ: "đang chờ xử lý"
) thì nó sẽ đưa ra một ngoại lệ bất cứ khi nào một chức năng tương ứng được gọi.
Chức năng của trình trợ giúp CMS
WordPress thực hiện một số chức năng của trình trợ giúp cũng phải được trừu tượng hóa, chẳng hạn như make_clickable
. Vì các hàm này rất chung chung, chúng tôi có thể thực hiện một hành vi mặc định cho chúng hoạt động tốt trong ngữ cảnh trừu tượng và có thể bị ghi đè nếu CMS thực hiện một giải pháp tốt hơn.
Trước tiên chúng tôi xác định hợp đồng:
giao diện HelperAPIInterface
{
chức năng công khai makeClickable (chuỗi $ văn bản);
}
Và cung cấp một hành vi mặc định cho các hàm trợ giúp thông qua một lớp trừu tượng:
lớp trừu tượng Tóm tắtHelperAPI thực hiện HelperAPIInterface
{
chức năng công khai makeClickable (chuỗi $ text) {
trả về preg numplace ('! (((f | ht) tp (s) ?: //) [-a-zA-Zа-яА-Я()0-9@:%_+.~#?&;//=] +)! i', ' $ 1 ', $ text);
}
}
Bây giờ, ứng dụng của chúng tôi có thể sử dụng chức năng này hoặc, nếu nó chạy trên WordPress, hãy sử dụng triển khai dành riêng cho WordPress:
lớp WPHelperAPI mở rộng Tóm tắt
{
chức năng công khai makeClickable (chuỗi $ text) {
trả về make_clickable ($ text);
}
}
Quyền của người dùng
Đối với tất cả các CMS hỗ trợ quản lý người dùng, ngoài việc trừu tượng hóa các chức năng tương ứng (chẳng hạn như current_user_can
và user_can
] trong WordPress), chúng tôi cũng phải đảm bảo rằng các quyền (hoặc khả năng) của người dùng có tác dụng tương tự trên tất cả các CMS. Để đạt được điều này, ứng dụng trừu tượng hóa của chúng ta cần nêu rõ những gì được mong đợi từ khả năng và việc triển khai cho mỗi CMS phải đáp ứng nó thông qua một trong các khả năng của chính nó hoặc đưa ra một ngoại lệ nếu nó có thể thỏa mãn nó. Chẳng hạn, nếu ứng dụng cần xác thực nếu người dùng có thể chỉnh sửa bài đăng, thì ứng dụng có thể thể hiện nó thông qua khả năng gọi là
"ability: editPosts"
được thỏa mãn cho WordPress thông qua khả năng của nó "edit_posts"
. (hoạt động trong thời gian biên dịch, do đó mã không biên dịch nếu một lớp thực hiện giao diện không thực hiện tất cả các chức năng được định nghĩa trong giao diện), PHP không cung cấp cấu trúc tương tự để xác thực khả năng hợp đồng (đơn giản là một chuỗi, chẳng hạn như "khả năng: editPosts"
) đã được thỏa mãn thông qua khả năng của CMS. Khái niệm này, mà tôi gọi là một hợp đồng lỏng lẻo, sẽ cần được xử lý bởi ứng dụng của chúng tôi, trong thời gian chạy.
Để giải quyết các hợp đồng lỏng lẻo, thông qua đó:
- ứng dụng có thể xác định những gì tên hợp đồng của Cameron phải thực hiện, thông qua chức năng
yêu cầuNames
. - việc triển khai cụ thể CMS có thể đáp ứng các tên đó , thông qua chức năng
execNames
. - ứng dụng có thể nhận được việc thực hiện một tên thông qua chức năng
getImcellenceedName
. - ứng dụng cũng có thể yêu cầu tất cả -tất cả các tên được yêu cầu thông qua chức năng
getNotImcellenceedRequiredNames
để ném một ngoại lệ hoặc ghi lại lỗi nếu cần.
Dịch vụ này trông như thế này:
lớp LooseContractService
{
được bảo vệ $ requiredNames = [];
được bảo vệ $ nameImcellenceations = [];
Hàm công khai allowNames (mảng $ name): void
{
$ this-> requiredNames = Array_merge (
$ this-> requiredNames,
$ tên
);
}
Hàm công khai execNames (mảng $ nameImcellenceations): void
{
$ this-> nameImcellenceations = Array_merge (
$ this-> nameImcellenceations,
$ nameImcellenceations
);
}
Hàm công khai getImcellenceedName (chuỗi $ name) :? string {
trả về $ this-> nameImcellenceations [$name];
}
hàm công khai getNotImcellenceedRequiredNames (): mảng {
trả về mảng_diff (
$ this-> requiredNames,
mảng_keys ($ this-> nameImcellenceations)
);
}
}
Ứng dụng, khi được khởi tạo, sau đó có thể thiết lập các hợp đồng lỏng lẻo bằng cách yêu cầu tên:
$ LooseContractService = ContainerBuilderFactory :: getInstance > get ('Loose_contuces');
$ LooseContractService-> requiredNames ([
'capability:editPosts',
]);
Và việc triển khai cụ thể CMS có thể đáp ứng những điều sau:
$ LooseContractService-> ]);
Sau đó, ứng dụng có thể phân giải tên được yêu cầu để triển khai từ CMS. Nếu tên bắt buộc này (trong trường hợp này, một khả năng) không được triển khai, thì ứng dụng có thể đưa ra một ngoại lệ:
$ cmsCapabilityName = $ LooseContractService-> getImcellenceedName ('ability: editPosts' );
if (! $ cmsCapabilityName) {
ném ngoại lệ mới (sprintf (
"CMS không hỗ trợ khả năng "% s "",
'khả năng: editPosts'
));
}
// Bây giờ có thể sử dụng khả năng để kiểm tra quyền
$ userManloymentAPIService = ContainerBuilderFactory :: getInstance () -> get ('user_man Quản lý_api');
if ($ userQuản lýAPIService-> userCan ($ user_id, $ cmsCapabilityName)) {
...
}
Ngoài ra, ứng dụng cũng có thể bị lỗi khi khởi tạo lần đầu nếu bất kỳ tên nào được yêu cầu không được thỏa mãn:
if ($ notIm vâyedNames = $ LooseContractService-> getNotImcellenceedRequiredNames ()) {
ném ngoại lệ mới (sprintf (
"CMS đã không triển khai tên hợp đồng lỏng lẻo% s",
implode (',', $ notImcellenceedNames)
));
}
Tùy chọn ứng dụng
Các tàu WordPress có một số tùy chọn ứng dụng, chẳng hạn như các tùy chọn được lưu trữ trong bảng wp_options
trong mục "mô tả blog"
"admin_email"
"date_format"
và nhiều thứ khác. Tóm tắt các tùy chọn ứng dụng bao gồm:
- trừu tượng hóa chức năng
getOption
; - trừu tượng hóa từng tùy chọn cần thiết, nhằm mục đích làm cho CMS thỏa mãn khái niệm này ví dụ: nếu một CMS không có tùy chọn cho mô tả của trang web, thì nó không thể trả lại tên của trang web đó.
Lần lượt giải quyết 2 hành động này. Chức năng liên quan getOption
tôi tin rằng chúng ta có thể mong đợi tất cả các CMS hỗ trợ các tùy chọn lưu trữ và truy xuất, vì vậy chúng ta có thể đặt chức năng tương ứng theo hợp đồng CMSCoreInterface
giao diện CMSCoreInterface
{
chức năng công khai getOption (tùy chọn $, $ default = false);
}
Vì có thể quan sát được từ chữ ký hàm ở trên, Iithm đưa ra giả định rằng mỗi tùy chọn cũng sẽ có giá trị mặc định. Tuy nhiên, tôi không biết nếu mọi CMS cho phép đặt giá trị mặc định cho các tùy chọn. Nhưng nó không thành vấn đề vì việc triển khai có thể đơn giản trả về NULL
sau đó.
Chức năng này được giải quyết cho WordPress như thế này:
lớp WPCMSCore thực hiện CMSCoreInter
{
chức năng công khai getOption (tùy chọn $, $ default = false)
{
trả về get_option (tùy chọn $, $ mặc định);
}
}
Để giải quyết hành động thứ 2, đó là trừu tượng hóa từng tùy chọn cần thiết, điều quan trọng cần lưu ý là mặc dù chúng ta luôn có thể mong đợi CMS hỗ trợ getOption
chúng ta có thể ' Chúng tôi hy vọng nó sẽ triển khai từng tùy chọn duy nhất được sử dụng bởi WordPress, chẳng hạn như "use_smiles"
hoặc "default_ping_status"
. Do đó, trước tiên chúng ta phải lọc tất cả các tùy chọn và chỉ trừu tượng những tùy chọn có ý nghĩa trong ngữ cảnh chung, chẳng hạn như "siteName"
hoặc "dateFormat"
.
, có danh sách các tùy chọn để trừu tượng, chúng ta có thể sử dụng một hợp đồng lỏng lẻo (như đã giải thích trước đó) và yêu cầu một tên tùy chọn tương ứng cho mỗi tùy chọn, chẳng hạn như "tùy chọn: siteName"
(được giải quyết cho WordPress như "blogname"
) hoặc "tùy chọn: dateFormat"
(được giải quyết là "date_format"
).
] Trong WordPress, khi chúng tôi yêu cầu dữ liệu từ hàm get_posts
chúng tôi có thể đặt thuộc tính "orderby"
trong $ args
để đặt kết quả, có thể dựa trên một cột từ bảng bài viết (chẳng hạn như các giá trị "ID"
"title"
"date"
"comment_count"
v.v.), một meta giá trị (thông qua các giá trị "meta_value"
và "meta_value_num"
) hoặc các giá trị khác (chẳng hạn như "post__in"
và "rand"
Bất cứ khi nào giá trị tương ứng với tên cột của bảng, chúng ta có thể trừu tượng hóa chúng bằng cách sử dụng một hợp đồng lỏng lẻo, như đã giải thích trước đó. Sau đó, ứng dụng có thể tham chiếu một tên hợp đồng lỏng lẻo:
$ args = [
'orderby' => $looseContractService->getImplementedName('dbcolumn:orderby:posts:date'),
];
$ post = $ postAPIService-> getPosts ($ args);
Và tên này được giải quyết cho WordPress:
$ LooseContractService-> ]);
Bây giờ, giả sử rằng trong ứng dụng WordPress của chúng tôi, chúng tôi đã tạo ra một giá trị meta "thích_count"
(lưu trữ bao nhiêu lượt thích bài đăng) phổ biến, và chúng tôi cũng muốn trừu tượng hóa chức năng này. Để đặt hàng kết quả theo một số thuộc tính meta, WordPress mong đợi một thuộc tính bổ sung "meta_key"
như thế này:
$ args = [
'orderby' => 'meta_value',
'meta_key' => 'likes_count',
];
[1945907] ] Vì thuộc tính bổ sung này, tôi xem xét việc triển khai WordPress cụ thể này và rất khó để trừu tượng hóa để làm cho nó hoạt động ở mọi nơi. Sau đó, thay vì khái quát hóa chức năng này, tôi chỉ có thể mong đợi mọi CMS sẽ thêm phần triển khai cụ thể của riêng mình.
Hãy để điều đó làm điều đó. Đầu tiên, tôi tạo một lớp trình trợ giúp để truy vấn truy vấn không xác định CMS:
lớp QueryHelper
{
Hàm công khai getOrderByQuery ()
{
trả về mảng (
'orderby' => $ LooseContractService-> getImcellenceedName ('dbcolumn: orderby: post: thíchCount'),
);
}
}
Gói dành riêng cho OctCMS có thể thêm một cột "thích_count"
vào bảng bài đăng và giải quyết tên "dbcolumn: orderby: bài viết: thíchCount "
đến " like_count "
và nó sẽ hoạt động. Mặc dù vậy, gói dành riêng cho WordPress phải giải quyết "dbcolumn: orderby: post: thíchCount"
là "meta_value"
và sau đó ghi đè chức năng của trình trợ giúp để thêm thuộc tính bổ sung " meta_key "
:
lớp WPQueryHelper mở rộng QueryHelper
{
Hàm công khai getOrderByQuery ()
{
$ query = Parent :: getOrderByQuery ();
$ truy vấn ['meta_key'] = 'thích_count';
trả về $ truy vấn;
}
}
Cuối cùng, chúng tôi đã thiết lập lớp truy vấn của người trợ giúp như một dịch vụ trong ContainerBuilder
định cấu hình nó để được phân giải thành lớp dành riêng cho WordPress và chúng tôi có được truy vấn để đặt hàng kết quả:
$ queryHelperService = ContainerBuilderFactory :: getInstance () -> get ('query_helper');
$ args = $ queryHelperService-> getOrderByQuery ();
$ post = $ postAPIService-> getPosts ($ args);
Tóm tắt các giá trị để đặt hàng kết quả không tương ứng với tên cột hoặc thuộc tính meta (chẳng hạn như "post__in"
và "rand"
) dường như khó khăn hơn. Because my application doesn’t use them, I haven’t considered how to do it, or even if it is possible. Then I took the easy way out: I have considered these to be WordPress-specific, hence the application makes them available only when running on WordPress.
Errors
When dealing with errors, we must consider abstracting the following elements:
- the definition of an error;
- error codes and messages.
Let’s review these in turn.
Definition of an error:
An Error
is a special object, different than an Exception
used to indicate that some operation has failed, and why it failed. WordPress represents errors through class WP_Error
and allows to check if some returned value is an error through function is_wp_error
.
We can abstract checking for an error:
interface CMSCoreInterface
{
public function isError($object);
}
Which is resolved for WordPress like this:
class WPCMSCore implements CMSCoreInterface
{
public function isError($object)
{
return is_wp_error($object);
}
}
However, to deal with errors in our abstracted code, we can’t expect all CMSs to have an error class with the same properties and methods as WordPress’s WP_Error
class. Hence, we must abstract this class too, and convert from the CMS error to the abstracted error after executing a function from the CMS.
The abstract error class Error
is simply a slightly modified version from WordPress’s WP_Error
class:
class Error {
protected $errors = array();
protected $error_data = array();
public function __construct($code = null, $message = null, $data = null)
{
if ($code) {
$this->errors[$code][] = $message;
if ($data) {
$this->error_data[$code] = $data;
}
}
}
public function getErrorCodes()
{
return array_keys($this->errors);
}
public function getErrorCode()
{
if ($codes = $this->getErrorCodes()) {
return $codes[0];
}
return null;
}
public function getErrorMessages($code = null)
{
if ($code) {
return $this->errors[$code] ?? [];
}
// Return all messages if no code specified.
return array_reduce($this->errors, 'array_merge', array());
}
public function getErrorMessage($code = null)
{
if (!$code) {
$code = $this->getErrorCode();
}
$messages = $this->getErrorMessages($code);
return $messages[0] ?? '';
}
public function getErrorData($code = null)
{
if (!$code) {
$code = $this->getErrorCode();
}
return $this->error_data[$code];
}
public function add($code, $message, $data = null)
{
$this->errors[$code][] = $message;
if ($data) {
$this->error_data[$code] = $data;
}
}
public function addData($data, $code = null)
{
if (!$code) {
$code = $this->getErrorCode();
}
$this->error_data[$code] = $data;
}
public function remove($code)
{
unset($this->errors[$code]);
unset($this->error_data[$code]);
}
}
We implement a function to convert from the CMS to the abstract error through a helper class:
class WPHelpers
{
public static function returnResultOrConvertError($result)
{
if (is_wp_error($result)) {
// Create a new instance of the abstracted error class
$error = new Error();
foreach ($result->get_error_codes() as $code) {
$error->add($code, $result->get_error_message($code), $result->get_error_data($code));
}
return $error;
}
return $result;
}
}
And we finally invoke this method for all functions that may return an error:
class UserManagementService implements UserManagementInterface
{
public function getPasswordResetKey($user_id)
{
$result = get_password_reset_key($user_id);
return WPHelpers::returnResultOrConvertError($result);
}
}
Error codes and messages:
Every CMS will have its own set of error codes and corresponding explanatory messages. For instance, WordPress function get_password_reset_key
can fail due to the following reasons, as represented by their error codes and messages:
"no_password_reset"
: Password reset is not allowed for this user."no_password_key_update"
: Could not save password reset key to database.
In order to unify errors so that an error code and message is consistent across CMSs, we will need to inspect these and replace them with our custom ones (possibly in function returnResultOrConvertError
explained above).
Hooks
Abstracting hooks involves:
- the hook functionality;
- the hooks themselves.
Let’s analyze these in turn.
Abstracting the hook functionality
WordPress offers the concept of “hooks”: a mechanism through which we can change a default behavior or value (through “filters”) and execute related functionality (through “actions”). Both Symfony and Laravel offer mechanisms somewhat related to hooks: Symfony provides an event dispatcher component, and Laravel’s mechanism is called events; these 2 mechanisms are similar, sending notifications of events that have already taken place, to be processed by the application through listeners.
When comparing these 3 mechanisms (hooks, event dispatcher and events) we find that WordPress’s solution is the simpler one to set-up and use: Whereas WordPress hooks enable to pass an unlimited number of parameters in the hook itself and to directly modify a value as a response from a filter, Symfony’s component requires to instantiate a new object to pass additional information, and Laravel’s solution suggests to run a command in Artisan (Laravel’s CLI) to generate the files containing the event and listener objects. If all we desire is to modify some value in the application, executing a hook such as $value = apply_filters("modifyValue", $value, $post_id);
is as simple as it can get.
In the first part of this series, I explained that the CMS-agnostic application already establishes a particular solution for dependency injection instead of relying on the solution by the CMS, because the application itself needs this functionality to glue its parts together. Something similar happens with hooks: they are such a powerful concept that the application can greatly benefit by making it available to the different CMS-agnostic packages (allowing them to interact with each other) and not leave this wiring-up to be implemented only at the CMS level. Hence, I have decided to already ship a solution for the “hook” concept in the CMS-agnostic application, and this solution is the one implemented by WordPress.
In order to decouple the CMS-agnostic hooks from those from WordPress, once again we must “code against interfaces, not implementations”: We define a contract with the corresponding hook functions:
interface HooksAPIInterface
{
public function addFilter(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void;
public function removeFilter(string $tag, $function_to_remove, int $priority = 10): bool;
public function applyFilters(string $tag, $value, ...$args);
public function addAction(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void;
public function removeAction(string $tag, $function_to_remove, int $priority = 10): bool;
public function doAction(string $tag, ...$args): void;
}
Please notice that functions applyFilters
and doAction
are variadic, i.e. they can receive a variable amount of arguments through parameter ...$args
. By combining this feature (which was added to PHP in version 5.6, hence it was unavailable to WordPress until very recently) with argument unpacking, i.e. passing a variable amount of parameters ...$args
to a function, we can easily provide the implementation for WordPress:
class WPHooksAPI implements HooksAPIInterface
{
public function addFilter(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void
{
add_filter($tag, $function_to_add, $priority, $accepted_args);
}
public function removeFilter(string $tag, $function_to_remove, int $priority = 10): bool
{
return remove_filter($tag, $function_to_remove, $priority);
}
public function applyFilters(string $tag, $value, ...$args)
{
return apply_filters($tag, $value, ...$args);
}
public function addAction(string $tag, $function_to_add, int $priority = 10, int $accepted_args = 1): void
{
add_action($tag, $function_to_add, $priority, $accepted_args);
}
public function removeAction(string $tag, $function_to_remove, int $priority = 10): bool
{
return remove_action($tag, $function_to_remove, $priority);
}
public function doAction(string $tag, ...$args): void
{
do_action($tag, ...$args);
}
}
As for an application running on Symfony or Laravel, this contract can be satisfied by installing a CMS-agnostic package implementing WordPress-like hooks, such as this one, this one or this one.
Finally, whenever we need to execute a hook, we do it through the corresponding service:
$hooksAPIService = ContainerBuilderFactory::getInstance()->get('hooks_api');
$title = $hooksAPIService->applyFilters("modifyTitle", $title, $post_id);
Abstracting the hooks themselves
We need to make sure that, whenever a hook is executed, a consistent action will be executed no matter which is the CMS. For hooks defined inside of our application that is no problem, since we can resolve them ourselves, most likely in our CMS-agnostic package. However, when the hook is provided by the CMS, such as action "init"
(triggered when the system has been initialized) or filter "the_title"
(triggered to modify a post’s title) in WordPress, and we invoke these hooks, we must make sure that all other CMSs will process them correctly and consistently. (Please notice that this concerns hooks that make sense in every CMS, such as "init"
; certain other hooks can be considered too specific to WordPress, such as filter "rest_{$this->post_type}_query"
from a REST controller, so we don’t need to abstract them.)
The solution I found is to hook into actions or filters defined exclusively in the application (i.e. not in the CMS), and to bridge from CMS hooks to application hooks whenever needed. For instance, instead of adding an action for hook "init"
(as defined in WordPress), any code in our application must add an action on hook "cms:init"
and then we implement the bridge in the WordPress-specific package from "init"
to "cms:init"
:
$hooksAPIService->addAction('init', function() use($hooksAPIService) {
$hooksAPIService->doAction('cms:init');
});
Finally, the application can add a “loose contract” name for "cms:init"
and the CMS-specific package must implement it (as demonstrated earlier on).
Routing
Different frameworks will provide different solutions for routing (i.e. the mechanism of identifying how the requested URL will be handled by the application), which reflect the architecture of the framework:
- In WordPress, URLs map to database queries, not to routes.
- Symfony provides a Routing component which is independent (any PHP application can install it and use it), and which enables to define custom routes and which controller will process them.
- Laravel’s routing builds on top of Symfony’s routing component to adapt it to the Laravel framework.
As it can be seen, WordPress’s solution is the outlier here: the concept of mapping URLs to database queries is tightly coupled to WordPress’s architecture, and we would not want to restrict our abstracted application to this methodology (for instance, October CMS can be set-up as a flat-file CMS, in which case it doesn’t use a database). Instead, it makes more sense to use Symfony’s approach as its default behavior, and allow WordPress to override this behavior with its own routing mechanism.
(Indeed, while WordPress’s approach works well for retrieving content, it is rather inappropriate when we need to access some functionality, such as displaying a contact form. In this case, before the launch of Gutenberg, we were forced to create a page and add a shortcode "[contact_form]"
to it as content, which is not as clean as simply mapping the route to its corresponding controller directly.)
Hence, the routing for our abstracted application will not be based around the modeled entities (post, page, category, tag, author) but purely on custom-defined routes. This should already work perfectly for Symfony and Laravel, using their own solutions, and there is not much for us to do other than injecting the routes with the corresponding controllers into the application’s configuration.
To make it work in WordPress, though, we need to take some extra steps: We must introduce an external library to handle routing, such as Cortex. Making use of Cortex, the application running on WordPress can have it both ways:
- if there is a custom-defined route matching the requested URL, use its corresponding controller.
- if not, let WordPress handle the request in its own way (i.e. retrieving the matched database entity or returning a 404 if no match is successful).
To implement this functionality, I have designed the contract CMSRoutingInterface
to, given the requested URL, calculate two pieces of information:
- the actual route, such as
contact
posts
orposts/my-first-post
. - the nature of the route: core nature values
"standard"
"home"
and"404"
and additional nature values added through packages such as"post"
through a “Posts” package or"user"
through a “Users” package.
The nature of the route is an artificial construction that enables the CMS-agnostic application to identify if the route has extra qualities attached to it. For instance, when requesting the URL for a single post in WordPress, the corresponding database object post is loaded into the global state, under global $post
. It also helps identify which case we want to handle, to avoid inconsistencies. For instance, we could have defined a custom route contact
handled by a controller, which will have nature "standard"
and also a page in WordPress with slug "contact"
which will have nature "page"
(added through a package called “Pages”). Then, our application can prioritize which way to handle the request, either through the controller or through a database query.
Let’s implement it. We first define the service’s contract:
interface CMSRoutingInterface
{
public function getNature();
public function getRoute();
}
We can then define an abstract class which provides a base implementation of these functions:
abstract class AbstractCMSRouting implements CMSRoutingInterface
{
const NATURE_STANDARD = 'standard';
const NATURE_HOME = 'home';
const NATURE_404 = '404';
public function getNature()
{
return self::NATURE_STANDARD;
}
public function getRoute()
{
// By default, the URI path is already the route (minus parameters and trailing slashes)
$route = $_SERVER['REQUEST_URI'];
$params_pos = strpos($route, '?');
if ($params_pos !== false) {
$route = substr($route, 0, $params_pos);
}
return trim($route, '/');
}
}
And the implementation is overriden for WordPress:
class WPCMSRouting extends AbstractCMSRouting
{
const ROUTE_QUERY = [
'custom_route_key' => 'custom_route_value',
];
private $query;
private function init()
{
if (is_null($this->query)) {
global $wp_query;
$this->query = $wp_query;
}
}
private function isStandardRoute() {
return !empty(array_intersect($this->query->query_vars, self::ROUTE_QUERY));
}
public function getNature()
{
$this->init();
if ($this->isStandardRoute()) {
return self::NATURE_STANDARD;
} elseif ($this->query->is_home() || $this->query->is_front_page()) {
return self::NATURE_HOME;
} elseif ($this->query->is_404()) {
return self::NATURE_404;
}
// Allow components to implement their own natures
$hooksAPIService = ContainerBuilderFactory::getInstance()->get('hooks_api');
return $hooksAPIService->applyFilters(
"nature",
parent::getNature(),
$this->query
);
}
}
In the code above, please notice how constant ROUTE_QUERY
is used by the service to know if the route is a custom-defined one, as configured through Cortex:
$hooksAPIService->addAction(
'cortex.routes',
function(RouteCollectionInterface $routes) {
// Hook into filter "routes" to provide custom-defined routes
$appRoutes = $hooksAPIService->applyFilters("routes", []);
foreach ($appRoutes as $route) {
$routes->addRoute(new QueryRoute(
$route,
function (array $matches) {
return WPCMSRouting::ROUTE_QUERY;
}
));
}
}
);
Finally, we add our routes through hook "routes"
:
$hooksAPIService->addFilter(
'routes',
function($routes) {
return array_merge(
$routes,
[
'contact',
'posts',
]
);
}
);
Now, the application can find out the route and its nature, and proceed accordingly (for instance, for a "standard"
nature invoke its controller, or for a "post"
nature invoke WordPress’s templating system):
$cmsRoutingService = ContainerBuilderFactory::getInstance()->get('routing');
$nature = $cmsRoutingService->getNature();
$route = $cmsRoutingService->getRoute();
// Process the requested route, as appropriate
// ...
Object properties
A rather inconvenient consequence of abstracting our code is that we can’t reference the properties from an object directly, and we must do it through a function instead. This is because different CMSs will represent the same object as containing different properties, and it is easier to abstract a function to access the object properties than to abstract the object itself (in which case, among other disadvantages, we may have to reproduce the object caching mechanism from the CMS). For instance, a post object $post
contains its ID under $post->ID
in WordPress and under $post->id
in October CMS. To resolve this property, our contract PostObjectPropertyResolverInterface
will contain function getId
:
interface PostObjectPropertyResolverInterface {
public function getId($post);
}
Which is resolved for WordPress like this:
class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {
public function getId($post)
{
return $post->ID;
}
}
Similarly, the post content property is $post->post_content
in WordPress and $post->content
in October CMS. Our contract will then allow to access this property through function getContent
:
interface PostObjectPropertyResolverInterface {
public function getContent($post);
}
Which is resolved for WordPress like this:
class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {
public function getContent($post)
{
return $post->post_content;
}
}
Please notice that function getContent
receives the object itself through parameter $post
. This is because we are assuming the content will be a property of the post object in all CMSs. However, we should be cautious on making this assumption, and decide on a property by property basis. If we don’t want to make the previous assumption, then it makes more sense for function getContent
to receive the post’s ID instead:
interface PostObjectPropertyResolverInterface {
public function getContent($post_id);
}
Being more conservative, the latter function signature makes the code potentially more reusable, however it is also less efficient, because the implementation will still need to retrieve the post object:
class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {
public function getContent($post_id)
{
$post = get_post($post_id);
return $post->post_content;
}
}
In addition, some properties may be needed in their original value and also after applying some processing; for these cases, we will need to implement a corresponding extra function in our contract. For instance, the post content needs be accessed also as HTML, which is done through executing apply_filters('the_content', $post->post_content)
in WordPress, or directly through property $post->content_html
in October CMS. Hence, our contract may have 2 functions to resolve the content property:
interface PostObjectPropertyResolverInterface {
public function getContent($post_id); // = raw content
public function getHTMLContent($post_id);
}
We must also be concerned with abstracting the value that the property can have. For instance, a comment is approved in WordPress if its property comment_approved
has the value "1"
. However, other CMSs may have a similar property with value true
. Hence, the contract should remove any potential inconsistency or ambiguity:
interface CommentObjectPropertyResolverInterface {
public function isApproved($comment);
}
Which is implemented for WordPress like this:
class WPCommentObjectPropertyResolver implements CommentObjectPropertyResolverInterface {
public function isApproved($comment)
{
return $comment->comment_approved == "1";
}
}
Global state
WordPress sets several variables in the global context, such as global $post
when querying a single post. Keeping variables in the global context is considered an anti-pattern, since the developer could unintentionally override their values, producing bugs that are difficult to track down. Hence, abstracting our code gives us the chance to implement a better solution.
An approach we can take is to create a corresponding class AppState
which simply contains a property to store all variables that our application will need. In addition to initializing all core variables, we enable components to initialize their own ones through hooks:
class AppState
{
public static $vars = [];
public static function getVars()
{
return self::$vars;
}
public static function initialize()
{
// Initialize core variables
self::$vars['nature'] = $cmsRoutingService->getNature();
self::$vars['route'] = $cmsRoutingService->getRoute();
// Initialize $vars through hooks
self::$vars = $hooksAPIService->applyFilters("AppState:init", self::$vars);
return self::$vars;
}
}
To replace global $post
a hook from WordPress can then set this data through a hook. A first step would be to set the data under "post-id"
:
$hooksAPIService->addFilter(
"AppState:init",
function($vars) {
if (is_single()) {
global $post;
$vars['post-id'] => $post->ID;
}
return $vars;
}
);
However, we can also abstract the global variables: instead of dealing with fixed entities (such as posts, users, comments, etc), we can deal with the entity in a generic way through "object-id"
and we obtain its properties by inquiring the nature of the requested route:
$hooksAPIService->addFilter(
"AppState:init",
function($vars) {
if ($vars['nature'] == 'post') {
global $post;
$vars['object-id'] => $post->ID;
}
return $vars;
}
);
From now own, if we need to display a property of the current post, we access it from the newly defined class instead of the global context:
$vars = AppState::getVars();
$object_id = $vars['object-id'];
// Do something with it
// ...
Entity models (meta, post types, pages being posts, and taxonomies —tags and categories—)
We must abstract those decisions made for WordPress concerning how its entities are modeled. Whenever we consider that WordPress’s opinionatedness makes sense in a generic context too, we can then replicate such a decision for our CMS-agnostic code.
Meta:
As mentioned earlier, the concept of “meta” must be decoupled from the model entity (such as “post meta” from “posts”), so if a CMS doesn’t provide support for meta, it can then discard only this functionality.
Then, package “Post Meta” (decoupled from, but dependent on, package “Posts”) defines the following contract:
interface PostMetaAPIInterface
{
public function getMetaKey($meta_key);
public function getPostMeta($post_id, $key, $single = false);
public function deletePostMeta($post_id, $meta_key, $meta_value = '');
public function addPostMeta($post_id, $meta_key, $meta_value, $unique = false);
public function updatePostMeta($post_id, $meta_key, $meta_value);
}
Which is resolved for WordPress like this:
class WPPostMetaAPI implements PostMetaAPIInterface
{
public function getMetaKey($meta_key)
{
return '_'.$meta_key;
}
public function getPostMeta($post_id, $key, $single = false)
{
return get_post_meta($post_id, $key, $single);
}
public function deletePostMeta($post_id, $meta_key, $meta_value = '')
{
return delete_post_meta($post_id, $meta_key, $meta_value);
}
public function addPostMeta($post_id, $meta_key, $meta_value, $unique = false)
{
return add_post_meta($post_id, $meta_key, $meta_value, $unique);
}
public function updatePostMeta($post_id, $meta_key, $meta_value)
{
return update_post_meta($post_id, $meta_key, $meta_value);
}
}
Post types:
I have decided that WordPress’s concept of a custom post type, which allows to model entities (such as an event or a portfolio) as extensions of posts, can apply in a generic context, and as such, I have replicated this functionality in the CMS-agnostic code. This decision is controversial, however, I justify it because the application may need to display a feed of entries of different types (such as posts, events, etc) and custom post types make such implementation feasible. Without custom post types, I would expect the application to need to execute several queries to bring the data for every entity type, and the logic would get all muddled up (for instance, if fetching 12 entries, should we fetch 6 posts and 6 events? but what if the events were posted much earlier than the last 12 posts? and so on).
What happens when the CMS doesn’t support this concept? Well, nothing serious happens: a post will still indicate its custom post type to be a “post”, and no other entities will inherit from the post. The application will still work properly, just with some slight overhead from the unneeded code. This is a trade-off that, I believe, is more than worth it.
To support custom post types, we simply add a function getPostType
in our contract:
interface PostAPIInterface
{
public function getPostType($post_id);
}
Which is resolved for WordPress like this:
class WPPostAPI implements PostAPIInterface
{
public function getPostType($post_id) {
return get_post_type($post_id);
}
}
Pages being posts:
While I justify keeping custom post types in order to extend posts, I don’t justify a page being a post, as it happens in WordPress, because in other CMSs these entities are completely decoupled and, more importantly, a page may have higher rank than a post, so making a page extend from a post would make no sense. For instance, October CMS ships pages in its core functionality, but posts must be installed through plugins.
Hence we must create separate contracts for posts and pages, even though they may contain the same functions:
interface PostAPIInterface
{
public function getTitle($post_id);
}
interface PageAPIInterface
{
public function getTitle($page_id);
}
To resolve these contracts for WordPress and avoid duplicating code, we can implement the common functionality through a trait:
trait WPCommonPostFunctions
{
public function getTitle($post_id)
{
return get_the_title($post_id);
}
}
class WPPostAPI implements PostAPIInterface
{
use WPCommonPostFunctions;
}
class WPPageAPI implements PageAPIInterface
{
use WPCommonPostFunctions;
}
Taxonomies (tags and categories):
Once again, we can’t expect all CMSs to support what is called taxonomies in WordPress: tags and categories. Hence, we must implement this functionality through a package “Taxonomies”, and, assuming that tags and categories are added to posts, make this package dependent on package “Posts”.
interface TaxonomyAPIInterface
{
public function getPostCategories($post_id, $options = []);
public function getPostTags($post_id, $options = []);
public function getCategories($query, $options = []);
public function getTags($query, $options = []);
public function getCategory($cat_id);
public function getTag($tag_id);
...
}
We could have decided to create two separate packages “Categories” and “Tags” instead of “Taxonomies”, however, as the implementation in WordPress makes evident, a tag and a category are basically the same concept of entity with only a tiny difference: categories are hierarchical (i.e. a category can have a parent category), but tags are not. Then, I consider that it makes sense to keep this concept for a generic context, and shipped under a single package “Taxonomies”.
We must pay attention that certain functionalities involve both posts and taxonomies, and these must be appropriately decoupled. For instance, in WordPress we can retrieve posts that were tagged "politics"
by executing get_posts(['tag' => "politics"])
. In this case, while function getPosts
must be implemented in package “Posts”, filtering by tags must be implemented in package “Taxonomies”. To accomplish this separation, we can simply execute a hook in the implementation of function getPosts
for WordPress, allowing any component to modify the arguments before executing get_posts
:
class WPPostAPI implements PostAPIInterface
{
public function getPosts($args) {
$args = $hooksAPIService->applyFilters("modifyArgs", $args);
return get_posts($args);
}
}
And finally we implement the hook in package “Taxonomies for WordPress”:
$hooksAPIService->addFilter(
'modifyArgs',
function($args) {
if (isset($args['tags'])) {
$args['tag'] = implode(',', $args['tags']);
unset($args['tags']);
}
if (isset($args['categories'])) {
$args['cat'] = implode(',', $args['categories']);
unset($args['categories']);
}
return $args;
}
);
Please notice that in the abstracted code the attributes were re-defined (following the recommendations for abstracting function parameters, explained earlier on): "tag"
must be provided as "tags"
and "cat"
must be provided as "categories"
(shifting the connotation from singular to plural), and these values must be passed as arrays (i.e. removed accepting comma-separated strings as in WordPress, to add consistency).
Translation
Because calls to translate strings are spread all over the application code, translation is not a functionality that we can opt out from, and we should make sure that the other frameworks are compatible with our chosen translation mechanism.
In WordPress, which implements internationalization through gettext, we are required to set-up translation files for each locale code (such as ‘fr_FR’, which is the code for french language from FRance), and these can be set under a text domain (which allows themes or plugins to define their own translations without fear of collision with the translations from other pieces of code). We don’t need to check for support for placeholders in the string to translate (such as when doing sprintf(__("Welcome %s"), $user_name)
), because function sprintf
belongs to PHP and not to the CMS, so it will always work.
Let’s check if the other frameworks support the required two properties, i.e. getting the translation data for a specific locale composed of language and country, and under a specific text domain:
- Symfony’s translation component supports these two properties.
- The locale used in Laravel’s localization involves the language but not the country, and text domains are not supported (they could be replicated through overriding package language files, but the domain is not explicitly set, so the contract and the implementation would be inconsistent with each other).
However, luckily there is library Laravel Gettext which can replace Laravel’s native implementation with Symfony’s translation component. Hence, we got support for all frameworks, and we can rely on a WordPress-like solution.
We can then define our contract mirroring the WordPress function signatures:
interface TranslationAPIInterface
{
public function __($text, $domain = 'default');
public function _e($text, $domain = 'default');
}
The implementation of the contract for WordPress is like this:
class WPTranslationAPI implements TranslationAPIInterface
{
public function __($text, $domain = 'default')
{
return __($text, $domain);
}
public function _e($text, $domain = 'default')
{
_e($text, $domain);
}
}
And to use it in our application, we do:
$translationAPI = ContainerBuilderFactory::getInstance()->get('translation_api');
$text = $translationAPI->__("translate this", "my-domain");
Media
WordPress has media management as part of its core functionality, which represents a media element as an entity all by itself, and allows to manipulate the media element (such as cropping or resizing images), but we can’t expect all CMSs to have similar functionality. Hence, media management must be decoupled from the CMS core functionality.
For the corresponding contract, we can mirror the WordPress media functions, but removing WordPress’s opinionatedness. For instance, in WordPress, a media element is a post (with post type "attachment"
), but for the CMS-agnostic code it is not, hence the parameter must be $media_id
(or $image_id
) instead of $post_id
. Similarly, WordPress treats media as attachments to posts, but this doesn’t need to be the case everywhere, hence we can remove the word “attachment” from the function signatures. Finally, we can decide to keep the $size
of the image in the contract; if the CMS doesn’t support creating multiple image sizes for an image, then it can just fall back on its default value NULL
and nothing grave happens:
interface MediaAPIInterface
{
public function getImageSrcAndDimensions($image_id, $size = null): array;
public function getImageURL($image_id, $size = null): string;
}
The response by function getImageSrcAndDimensions
can be asbtracted too, returning an array of our own design instead of simply re-using the one from the WordPress function wp_get_attachment_image_src
:
class WPMediaAPI implements MediaAPIInterface
{
public function getImageSrcAndDimensions($image_id, $size = null): array
{
$img_data = wp_get_attachment_image_src($image_id, $size);
return [
'src' => $img_data[0],
'width' => $img_data[1],
'height' => $img_data[2],
];
}
public function getImageURL($image_id, $size = null): string
{
return wp_get_attachment_image_url($image_id, $size);
}
}
Conclusion
Setting-up a CMS-agnostic architecture for our application can be a painful endeavor. As it was demonstrated in this article, abstracting all the code was a lengthy process, taking plenty of time and energy to achieve, and it is not even finished yet. I wouldn’t be surprised if the reader is intimidated by the idea of going through this process in order to convert a WordPress application into a CMS-agnostic one. If I hadn’t done the abstraction myself, I would certainly be intimidated too.
My suggestion is for the reader is to analyze if going through this process makes sense based on a project-by-project basis. If there is no need whatsoever to port an application to a different CMS, then you will be right to stay away from this process and stick to the WordPress way. However, if you do need to migrate an application away from WordPress and want to reduce the effort required, or if you already need to maintain several codebases which would benefit from code reusability, or even if you may migrate the application sometime in the future and you have just started a new project, then this process is for you. It may be painful to implement, but well worth it. I know because I’ve been there. But I’ve survived, and I’d certainly do it again. Thanks for reading.
(dm, yk, il)
Source link: webdesignernews