Resumen del código de WordPress para reutilizar con otros CMS: Implementación (Parte 2)

Resumen del código de WordPress para reutilizar con otros CMS: Implementación (Parte 2)

En la primera parte de esta serie , aprendimos los conceptos clave para crear una aplicación que sea lo más independiente posible de CMS. En esta segunda y última parte, procederemos a abstraer una aplicación de WordPress, preparando su código para ser utilizado con los componentes de Symfony , el marco de trabajo de Laravel y el CMS de octubre (que se basa en Laravel).

Acceso A Servicios

Antes de comenzar a abstraer el código, debemos proporcionar la capa de inyección de dependencia a la aplicación. Como se describe en la primera parte de esta serie, esta capa se satisface a través del componente DependencyInjection de Symfony . Para acceder a los servicios definidos, creamos una clase ContainerBuilderFactoryque simplemente almacena una instancia estática del ContainerBuilderobjeto del componente :

use Symfony\Component\DependencyInjection\ContainerBuilder;

class ContainerBuilderFactory {
  private static $instance;
  public static function init()
  {
    self::$instance = new ContainerBuilder();
  }
  public static function getInstance()
  {
    return self::$instance;
  }
}

Luego, para acceder a un servicio llamado "cache", la aplicación lo solicita así:

$cacheService = ContainerBuilderFactory::getInstance()->get('cache');
// Do something with the service
// $cacheService->...

Resumen Del Código De WordPress

Hemos identificado los siguientes fragmentos de código y conceptos de una aplicación de WordPress que deben extraerse de la opinión de WordPress:

  • acceder a funciones
  • nombres de funciones
  • parámetros de función
  • estados (y otros valores constantes)
  • Funciones de ayuda de CMS
  • Permisos de usuario
  • opciones de aplicación
  • nombres de columna de base de datos
  • errores
  • manos
  • enrutamiento
  • propiedades del objeto
  • estado global
  • modelos de entidad (meta, tipos de publicación, páginas que son publicaciones y taxonomías —etiquetas y categorías—)
  • Traducción
  • medios de comunicación.

Procedamos a abstraerlos, uno por uno.

Nota: Para facilitar la lectura, he omitido agregar espacios de nombres a todas las clases e interfaces a lo largo de este artículo. Sin embargo, ¡agregar espacios de nombres, como se especifica en la Recomendación de estándares PHP PSR-4 , es imprescindible! Entre otras ventajas, la aplicación puede beneficiarse de la carga automática , y la inyección de dependencia de Symfony puede confiar en la carga automática del servicio para reducir su configuración al mínimo.

ACCESO A FUNCIONES

El mantra «código contra interfaces, no implementaciones» significa que ya no se puede acceder directamente a todas esas funciones proporcionadas por el CMS. En cambio, debemos acceder a la función desde un contrato (una interfaz), en el cual la función CMS será simplemente la implementación. Al final de la abstracción, dado que ya no se hará referencia directa a ningún código de WordPress, podemos intercambiar WordPress con un CMS diferente.

Por ejemplo, si nuestra aplicación accede a la función get_posts:

$posts = get_posts($args);

Luego debemos abstraer esta función bajo algún contrato:

interface PostAPIInterface
{
  public function getPosts($args);
}

El contrato debe implementarse para WordPress:

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args) {
    return get_posts($args);
  }
}

Se "posts_api"debe agregar un servicio al services.yamlarchivo de configuración de inyección de dependencia , que indica qué clase resuelve el servicio:

services:
  posts_api:
    class: \WPPostAPI

Y finalmente, la aplicación puede hacer referencia a la función a través del servicio "posts_api":

$postsAPIService = ContainerBuilderFactory::getInstance()->get('posts_api');
$posts = $postsAPIService->getPosts($args);

NOMBRES DE FUNCIONES

Si se ha dado cuenta del código demostrado anteriormente, la función get_postsse abstrae como getPosts. Hay un par de razones por las cuales esta es una buena idea:

  • Al llamar a la función de manera diferente, ayuda a identificar qué código pertenece a WordPress y qué código pertenece a nuestra aplicación abstraída.
  • Los nombres de funciones deben ser camelCased para cumplir con PSR-2 , que intenta definir un estándar para escribir código PHP.

Ciertas funciones pueden redefinirse, teniendo más sentido en un contexto abstracto. Por ejemplo, la función de WordPress get_user_by($field, $value)utiliza el parámetro $fieldcon valores de "id""ID""slug""email""login"para saber como llegar al usuario. En lugar de replicar esta metodología, podemos definir explícitamente una función separada para cada uno de ellos:

interface UsersAPIInterface
{
  public function getUserById($value);
  public function getUserByEmail($value);
  public function getUserBySlug($value);
  public function getUserByLogin($value);
}

Y estos se resuelven para WordPress:

class WPUsersAPI implements UsersAPIInterface
{
  public function getUserById($value)
  {
    return get_user_by('id', $value);
  }
  public function getUserByEmail($value)
  {
    return get_user_by('email', $value);
  }
  public function getUserBySlug($value)
  {
    return get_user_by('slug', $value);
  }
  public function getUserByLogin($value)
  {
    return get_user_by('login', $value);
  }
}

Ciertas otras funciones deben renombrarse porque sus nombres transmiten información sobre su implementación, que puede no aplicarse para un CMS diferente. Por ejemplo, la función de WordPress get_the_author_metapuede recibir parámetros "user_lastname", lo que indica que el apellido del usuario se almacena como un valor «meta» (que se define como una propiedad adicional para un objeto, no originalmente asignado en el modelo de base de datos). Sin embargo, otros CMS pueden tener una columna "lastname"en la tabla de usuario, por lo que no se aplica como un metavalor. (La definición real del valor «meta» es realmente inconsistente en WordPress: la función get_the_author_metatambién acepta el valor "user_email", aunque el correo electrónico esté almacenado en la tabla del usuario. Por lo tanto, prefiero seguir con mi definición del valor «meta» y eliminar todo inconsistencias del código abstraído).

Luego, nuestro contrato implementará las siguientes funciones:

interface UsersAPIInterface
{
  public function getUserDisplayName($user_id);
  public function getUserEmail($user_id);
  public function getUserFirstname($user_id);
  public function getUserLastname($user_id);
  ...
}

Que se resuelven para WordPress:

class WPUsersAPI implements UsersAPIInterface
{
  public function getUserDisplayName($user_id)
  {
    return get_the_author_meta('display_name', $user_id);
  }
  public function getUserEmail($user_id)
  {
    return get_the_author_meta('user_email', $user_id);
  }
  public function getUserFirstname($user_id)
  {
    return get_the_author_meta('user_firstname', $user_id);
  }
  public function getUserLastname($user_id)
  {
    return get_the_author_meta('user_lastname', $user_id);
  }
  ...
}

Nuestras funciones también podrían redefinirse para eliminar las limitaciones de WordPress. Por ejemplo, la función update_user_meta($user_id, $meta_key, $meta_value)puede recibir un meta atributo a la vez, lo que tiene sentido ya que cada uno de estos se actualiza en su propia consulta de base de datos. Sin embargo, el CMS de octubre asigna todos los meta atributos juntos en una sola columna de base de datos, por lo que tiene más sentido actualizar todos los valores juntos en una sola operación de base de datos. Entonces, nuestro contrato puede incluir una operación updateUserMetaAttributes($user_id, $meta)que puede actualizar varios meta valores al mismo tiempo:

interface UserMetaInterface
{
  public function updateUserMetaAttributes($user_id, $meta);
}

Lo que se resuelve para WordPress así:

class WPUsersAPI implements UsersAPIInterface
{
  public function updateUserMetaAttributes($user_id, $meta)
  {
    foreach ($meta as $meta_key => $meta_value) {
      update_user_meta($user_id, $meta_key, $meta_value);
    }
  }
}

Finalmente, podemos querer redefinir una función para eliminar sus ambigüedades. Por ejemplo, la función de WordPress add_query_argpuede recibir parámetros de dos maneras diferentes:

  1. Usando una sola clave y valor: add_query_arg('key', 'value', 'http://example.com');
  2. Usando una matriz asociativa: add_query_arg(['key1' => 'value1', 'key2' => 'value2'], 'http://example.com');

Esto se vuelve difícil de mantener consistente en los CMS. Por lo tanto, nuestro contrato puede definir funciones addQueryArg(singular) y addQueryArgs(plural) para eliminar la ambigüedad:

public function addQueryArg(string $key, string $value, string $url);
public function addQueryArgs(array $key_values, string $url);

PARÁMETROS DE LA FUNCIÓN

También debemos abstraer los parámetros a la función, asegurándonos de que tengan sentido en un contexto genérico. Para que cada función se abstraiga, debemos considerar:

  • renombrar y / o redefinir los parámetros;
  • renombrar y / o redefinir los atributos pasados ​​en los parámetros de la matriz.

Por ejemplo, la función de WordPress get_postsrecibe un parámetro único $args, que es una matriz de atributos. Uno de sus atributos es el fieldsque, cuando se le da el valor "ids", hace que la función devuelva una matriz de ID en lugar de una matriz de objetos. Sin embargo, considero que esta implementación es demasiado específica para WordPress, y para un contexto genérico preferiría una solución diferente: transmitir esta información a través de un parámetro separado llamado $options, bajo atributo "return-type".

Para lograr esto, agregamos parámetros $optionsa la función en nuestro contrato:

interface PostAPIInterface
{
  public function getPosts($args, $options = []);
}

En lugar de hacer referencia al valor constante de WordPress "ids"(que no podemos garantizar será el utilizado en todos los demás CMS), creamos un valor constante correspondiente para nuestra aplicación abstraída:

class Constants
{
  const RETURNTYPE_IDS = 'ids';
}

La implementación de WordPress debe mapear y recrear los parámetros entre el contrato y la implementación:

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args, $options = []) {
    if ($options['return-type'] == Constants::RETURNTYPE_IDS) {
      $args['fields'] = 'ids';
    }
    return get_posts($args);
  }
}

Y finalmente, podemos ejecutar el código a través de nuestro contrato:

$options = [
  'return-type' => Constants::RETURNTYPE_IDS,
];
$post_ids = $postsAPIService->getPosts($args, $options);

Al abstraer los parámetros, debemos evitar transferir la deuda técnica de WordPress a nuestro código resumido, siempre que sea posible. Por ejemplo, el parámetro $argsde la función get_postspuede contener atributo 'post_type'. Este nombre de atributo es algo engañoso, ya que puede recibir un elemento ( post_type => "post"), sino también una lista de ellos ( post_type => "post, event"), por lo que este nombre debe estar en plural en su lugar: post_types. Al abstraer este fragmento de código, podemos configurar nuestra interfaz para que espere un atributo post_types, que se asignará a WordPress post_type.

Del mismo modo, las diferentes funciones aceptan argumentos con diferentes nombres, aunque tengan el mismo objetivo, por lo que sus nombres pueden unificarse. Por ejemplo, a través del parámetro $args, la función de WordPress get_postsacepta el atributo posts_per_pagey la función get_usersacepta el atributo number. Estos nombres de atributos se pueden reemplazar perfectamente con el nombre de atributo más genérico limit.

También es una buena idea cambiar el nombre de los parámetros para que sea fácil entender cuáles pertenecen a WordPress y cuáles se han abstraído. Por ejemplo, podemos decidir reemplazar todo "_"con "-", por lo que nuestro argumento recién definido se post_typesconvierte en post-types.

Aplicando estas consideraciones anteriores, nuestro código resumido se verá así:

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args, $options = []) {
    ...
    if (isset($args['post-types'])) {
      $args['post_type'] = $args['post-types'];
      unset($args['post-types']);
    }
    if (isset($args['limit'])) { 
      $args['posts_per_page'] = $args['limit'];
      unset($args['limit']);
    }
    return get_posts($args);
  }
}

También podemos redefinir atributos para modificar la forma de sus valores. Por ejemplo, WordPress parámetro $argsen la función get_postspuede recibir atributo date_query, cuyas propiedades ( "after""inclusive", etc) puede considerarse específico para WordPress:

$date = current_time('timestamp');
$args['date_query'] = array(
  array(
    'after' => date('Y-m-d H:i:s', $date),
    'inclusive' => true,
  )
);

Para unificar la forma de este valor en algo más genérico, podemos volver a implementarlo usando otros argumentos, como "date-from""date-from-inclusive"(sin embargo, esta solución no es 100% convincente, ya que es más detallada que la de WordPress):

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args, $options = []) {
    ...
    if (isset($args['date-from'])) {
      $args['date_args'][] = [
        'after' => $args['date-from'],
        'inclusive' => false,
      ];
      unset($args['date-from']);
    }
    if (isset($args['date-from-inclusive'])) {
      $args['date_args'][] = [
        'after' => $args['date-from-inclusive'],
        'inclusive' => true,
      ];
      unset($args['date-from-inclusive']);
    }
    return get_posts($args);
  }
}

Además, debemos considerar si abstraer o no aquellos parámetros que son demasiado específicos para WordPress. Por ejemplo, la función get_postspermite ordenar publicaciones por atributo menu_order, lo que no creo que funcione en un contexto genérico. Entonces, prefiero no abstraer este código y mantenerlo en el paquete específico de CMS para WordPress.

Finalmente, también podemos agregar tipos de argumentos (y, dado que aquí estamos, también devolver tipos) a nuestra función de contrato, haciéndolo más comprensible y permitiendo que el código falle en el tiempo de compilación en lugar de durante el tiempo de ejecución:

interface PostAPIInterface
{
  public function getPosts(array $args, array $options = []): array;
}

ESTADOS (Y OTROS VALORES CONSTANTES)

Necesitamos asegurarnos de que todos los estados tengan el mismo significado en todos los CMS. Por ejemplo, los mensajes de WordPress pueden tener uno entre los siguientes estados: "publish""pending""draft""trash". Para asegurarnos de que la aplicación haga referencia a la versión resumida de los estados y no a la específica de CMS, simplemente podemos definir un valor constante para cada uno de ellos:

class PostStates {
  const PUBLISHED = 'published';
  const PENDING = 'pending';
  const DRAFT = 'draft';
  const TRASH = 'trash';
}

Como se puede ver, los valores constantes reales pueden o no ser los mismos que en WordPress: mientras "publish"se renombró como "published", los otros permanecen iguales.

Para la implementación de WordPress, convertimos del valor agnóstico al específico de WordPress:

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args, $options = []) {
    ...
    if (isset($args['post-status'])) {
      $conversion = [
        PostStates::PUBLISHED => 'publish',
        PostStates::PENDING => 'pending',
        PostStates::DRAFT => 'draft',
        PostStates::TRASH => 'trash',
      ];
      $args['post_status'] = $conversion[$args['post-status']];
      unset($args['post-status']);
    }
    return get_posts($args);
  }
}

Finalmente, podemos hacer referencia a estas constantes en toda nuestra aplicación independiente de CMS:

$args = [
  'post-status' => PostStates::PUBLISHED,
];
$posts = $postsAPIService->getPosts($args);

Esta estrategia funciona bajo el supuesto de que todos los CMS admitirán estos estados. Si algún CMS no admite un estado particular (p. Ej . "pending":), debería generar una excepción cada vez que se invoque la funcionalidad correspondiente.

FUNCIONES DE AYUDA DE CMS

WordPress implementa varias funciones auxiliares que también deben abstraerse, como make_clickable. Debido a que estas funciones son muy genéricas, podemos implementar un comportamiento predeterminado para ellas que funcione bien en un contexto abstracto y que se pueda anular si el CMS implementa una mejor solución.

Primero definimos el contrato:

interface HelperAPIInterface
{
  public function makeClickable(string $text);
}

Y proporcione un comportamiento predeterminado para las funciones auxiliares a través de una clase abstracta:

abstract class AbstractHelperAPI implements HelperAPIInterface
{
  public function makeClickable(string $text) {
    return preg_replace('!(((f|ht)tp(s)?://)[-a-zA-Zа-яА-Я()[email protected]:%_+.~#?&;//=]+)!i', '<a href="$1">$1</a>', $text);
  }
}

Ahora, nuestra aplicación puede usar esta funcionalidad o, si se ejecuta en WordPress, usar la implementación específica de WordPress:

class WPHelperAPI extends AbstractHelperAPI
{
  public function makeClickable(string $text) {
    return make_clickable($text);
  }
}

PERMISOS DE USUARIO

Para todos los CMS que admiten la gestión de usuarios, además de abstraer las funciones correspondientes (como current_user_canuser_canen WordPress), también debemos asegurarnos de que los permisos (o capacidades) del usuario tengan el mismo efecto en todos los CMS. Para lograr esto, nuestra aplicación resumida necesita establecer explícitamente lo que se espera de la capacidad, y la implementación de cada CMS debe satisfacerla a través de una de sus propias capacidades o lanzar una excepción si no puede satisfacerla. Por ejemplo, si la aplicación necesita validar si el usuario puede editar publicaciones, puede representarla a través de una capacidad llamada "capability:editPosts", que se satisface para WordPress a través de su capacidad "edit_posts".

Esto sigue siendo una instancia del principio de «código contra interfaces, no implementaciones», sin embargo, aquí encontramos un problema: mientras que en PHP podemos definir interfaces y clases para modelar contratos y proveedores de servicios (que funciona en tiempo de compilación, de modo que el código no se compila si una clase que implementa una interfaz no implementa todas las funciones definidas en la interfaz), PHP no ofrece una construcción similar para validar que una capacidad de contrato (que es simplemente una cadena, por ejemplo "capability:editPosts") ha sido satisfecha a través de una capacidad por El CMS. Este concepto, que llamo un «contrato suelto», deberá ser manejado por nuestra aplicación, en tiempo de ejecución.

Para tratar con «contratos sueltos», he creado un servicio a LooseContractServicetravés del cual:

  • la aplicación puede definir qué «nombres de contrato» deben implementarse, a través de la función requireNames.
  • Las implementaciones específicas de CMS pueden satisfacer esos nombres, a través de la función implementNames.
  • la aplicación puede obtener la implementación de un nombre a través de la función getImplementedName.
  • la aplicación también puede solicitar todos los nombres requeridos no satisfechos a través de la función getNotImplementedRequiredNames, como para lanzar una excepción o registrar el error si es necesario.

El servicio se ve así:

class LooseContractService
{
  protected $requiredNames = [];
  protected $nameImplementations = [];

  public function requireNames(array $names): void
  {
    $this->requiredNames = array_merge(
      $this->requiredNames,
      $names
    );
  }

  public function implementNames(array $nameImplementations): void
  {
    $this->nameImplementations = array_merge(
      $this->nameImplementations,
      $nameImplementations
    );
  }

  public function getImplementedName(string $name): ?string {
    return $this->nameImplementations[$name];
  }

  public function getNotImplementedRequiredNames(): array {
    return array_diff(
      $this->requiredNames,
      array_keys($this->nameImplementations)
    );
  }
}

La aplicación, cuando se inicializa, puede establecer contratos sueltos al requerir nombres:

$looseContractService = ContainerBuilderFactory::getInstance()->get('loose_contracts');
$looseContractService->requireNames([
  'capability:editPosts',
]);

Y la implementación específica de CMS puede satisfacer estos:

$looseContractService->implementNames([
  'capability:editPosts' => 'edit_posts',
]);

La aplicación puede resolver el nombre requerido para la implementación desde el CMS. Si este nombre requerido (en este caso, una capacidad) no se implementa, entonces la aplicación puede lanzar una excepción:

$cmsCapabilityName = $looseContractService->getImplementedName('capability:editPosts');
if (!$cmsCapabilityName) {
  throw new Exception(sprintf(
    "The CMS has no support for capability \"%s\"",
    'capability:editPosts'
  ));
}
// Now can use the capability to check for permissions
$userManagementAPIService = ContainerBuilderFactory::getInstance()->get('user_management_api');
if ($userManagementAPIService->userCan($user_id, $cmsCapabilityName)) {
  ...
}

Alternativamente, la aplicación también puede fallar cuando se inicializa por primera vez si alguno de los nombres requeridos no está satisfecho:

if ($notImplementedNames = $looseContractService->getNotImplementedRequiredNames()) {
  throw new Exception(sprintf(
    "The CMS has not implemented loose contract names %s",
    implode(', ', $notImplementedNames)
  ));
}

OPCIONES DE APLICACIÓN

WordPress barcos con varias opciones de aplicación, como los almacenados en la tabla wp_optionsdebajo de las entradas "blogname""blogdescription""admin_email""date_format"y muchos otros. Resumir las opciones de la aplicación implica:

  • abstracción de la función getOption;
  • abstraer cada una de las opciones requeridas, con el objetivo de hacer que el CMS satisfaga la noción de esta opción (por ejemplo: si un CMS no tiene una opción para la descripción del sitio, no puede devolver el nombre del sitio).

Resolvamos estas 2 acciones por turno. Con respecto a la función getOption, creo que podemos esperar que todos los CMS admitan las opciones de almacenamiento y recuperación, por lo que podemos colocar la función correspondiente en un CMSCoreInterfacecontrato:

interface CMSCoreInterface
{
  public function getOption($option, $default = false);
}

Como se puede observar en la firma de la función anterior, estoy asumiendo que cada opción también tendrá un valor predeterminado. Sin embargo, no sé si cada CMS permite establecer valores predeterminados para las opciones. Pero no importa ya que la implementación simplemente puede regresar NULLentonces.

Esta función se resuelve para WordPress así:

class WPCMSCore implements CMSCoreInterface
{
  public function getOption($option, $default = false)
  {
    return get_option($option, $default);
  }
}

Para resolver la segunda acción, que es abstraer cada opción necesaria, es importante tener en cuenta que, aunque siempre podemos esperar que el CMS sea compatible getOption, no podemos esperar que implemente cada una de las opciones utilizadas por WordPress, como "use_smiles""default_ping_status". Por lo tanto, primero debemos filtrar todas las opciones y abstraer solo aquellas que tengan sentido en un contexto genérico, como "siteName""dateFormat".

Luego, teniendo la lista de opciones para abstraer, podemos usar un «contrato suelto» (como se explicó anteriormente) y requerir un nombre de opción correspondiente para cada uno, como "option:siteName"(resuelto para WordPress como "blogname") o "option:dateFormat"(resuelto como "date_format").

NOMBRES DE COLUMNA DE BASE DE DATOS

En WordPress, cuando estamos solicitando datos de función get_postspodemos establecer atributo "orderby"en $argsordenar los resultados, que pueden basarse en una columna de la tabla de mensajes (tales como los valores "ID""title""date""comment_count", etc), un valor meta (a través de los valores "meta_value""meta_value_num") u otros valores (como "post__in""rand").

Siempre que el valor corresponda al nombre de la columna de la tabla, podemos abstraerlos usando un «contrato suelto», como se explicó anteriormente. Luego, la aplicación puede hacer referencia a un nombre de contrato suelto:

$args = [
  'orderby' => $looseContractService->getImplementedName('dbcolumn:orderby:posts:date'),
];
$posts = $postsAPIService->getPosts($args);

Y este nombre está resuelto para WordPress:

$looseContractService->implementNames([
  'dbcolumn:orderby:posts:date' => 'date',
]);

Ahora, digamos que en nuestra aplicación de WordPress hemos creado un metavalor "likes_count"(que almacena cuántos me gusta tiene una publicación) para ordenar publicaciones por popularidad, y también queremos abstraer esta funcionalidad. Para ordenar los resultados por alguna meta propiedad, WordPress espera un atributo adicional "meta_key", como este:

$args = [
  'orderby' => 'meta_value',
  'meta_key' => 'likes_count',
];

Debido a este atributo adicional, considero esta implementación específica de WordPress y muy difícil de abstraer para que funcione en todas partes. Luego, en lugar de generalizar esta funcionalidad, simplemente puedo esperar que cada CMS agregue su propia implementación específica.

Vamos a hacer eso. Primero, creo una clase auxiliar para recuperar la consulta independiente de CMS:

class QueryHelper
{
  public function getOrderByQuery()
  {
    return array(
      'orderby' => $looseContractService->getImplementedName('dbcolumn:orderby:posts:likesCount'),
    );
  }
}

El paquete OctoberCMS-específico puede agregar una columna "likes_count"a la tabla de mensajes, y el nombre de determinación "dbcolumn:orderby:posts:likesCount"de "like_count"y funcionará. Sin embargo, el paquete específico de WordPress debe resolverse "dbcolumn:orderby:posts:likesCount"como "meta_value"y luego anular la función auxiliar para agregar la propiedad adicional "meta_key":

class WPQueryHelper extends QueryHelper
{
  public function getOrderByQuery()
  {
    $query = parent::getOrderByQuery();
    $query['meta_key'] = 'likes_count';
    return $query;
  }
}

Finalmente, configuramos la clase de consulta auxiliar como un servicio en ContainerBuilder, la configuramos para que se resuelva en la clase específica de WordPress, y obtenemos la consulta para ordenar los resultados:

$queryHelperService = ContainerBuilderFactory::getInstance()->get('query_helper');
$args = $queryHelperService->getOrderByQuery();
$posts = $postsAPIService->getPosts($args);

Resumir los valores para ordenar resultados que no corresponden a nombres de columna o meta propiedades (como "post__in""rand") parece ser más difícil. Debido a que mi aplicación no los usa, no he considerado cómo hacerlo, o incluso si es posible. Luego tomé el camino fácil: he considerado que estos son específicos de WordPress, por lo tanto, la aplicación los hace disponibles solo cuando se ejecuta en WordPress.

ERRORES

Cuando se trata de errores, debemos considerar abstraer los siguientes elementos:

  • la definición de un error;
  • Códigos de error y mensajes.

Repasemos esto a su vez.

Definición de un error:

Un Errores un objeto especial, diferente de un Exception, utilizado para indicar que alguna operación ha fallado y por qué falló. WordPress representa errores a través de la clase WP_Errory permite verificar si algún valor devuelto es un error a través de la función is_wp_error.

Podemos hacer un resumen de la comprobación de un error:

interface CMSCoreInterface
{
  public function isError($object);
}

Lo que se resuelve para WordPress así:

class WPCMSCore implements CMSCoreInterface
{
  public function isError($object)
  {
    return is_wp_error($object);
  }
}

Sin embargo, para tratar los errores en nuestro código abstraído, no podemos esperar que todos los CMS tengan una clase de error con las mismas propiedades y métodos que la WP_Errorclase de WordPress . Por lo tanto, también debemos abstraer esta clase y convertir del error CMS al error abstraído después de ejecutar una función desde el CMS.

La clase de error abstracto Errores simplemente una versión ligeramente modificada de la WP_Errorclase de WordPress :

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]);
  }
}

Implementamos una función para convertir del CMS al error abstracto a través de una clase auxiliar:

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;
  }
}

Y finalmente invocamos este método para todas las funciones que pueden devolver un error:

class UserManagementService implements UserManagementInterface
{
  public function getPasswordResetKey($user_id)
  {
    $result = get_password_reset_key($user_id);
    return WPHelpers::returnResultOrConvertError($result);
  }
}
Códigos de error y mensajes:

Cada CMS tendrá su propio conjunto de códigos de error y los mensajes explicativos correspondientes. Por ejemplo, la función de WordPress get_password_reset_keypuede fallar debido a las siguientes razones, como lo representan sus códigos y mensajes de error:

  1. "no_password_reset": El restablecimiento de contraseña no está permitido para este usuario.
  2. "no_password_key_update": No se pudo guardar la clave de restablecimiento de contraseña en la base de datos.

Para unificar los errores de modo que un código de error y un mensaje sean consistentes en los CMS, tendremos que inspeccionarlos y reemplazarlos por nuestros personalizados (posiblemente en función returnResultOrConvertErrorexplicada anteriormente).

MANOS

Resumen de ganchos implica:

  • la funcionalidad de gancho;
  • los ganchos mismos

Analicemos estos a su vez.

Resumen de la funcionalidad del gancho

WordPress ofrece el concepto de «ganchos»: un mecanismo a través del cual podemos cambiar un comportamiento o valor predeterminado (a través de «filtros») y ejecutar funcionalidades relacionadas (a través de «acciones»). Tanto Symfony como Laravel ofrecen mecanismos algo relacionados con los ganchos: Symfony proporciona un componente de despachador de eventos , y el mecanismo de Laravel se llama eventos ; Estos 2 mecanismos son similares y envían notificaciones de eventos que ya han tenido lugar para que la aplicación los procese a través de los oyentes.

Al comparar estos 3 mecanismos (ganchos, despachador de eventos y eventos), encontramos que la solución de WordPress es la más simple de configurar y usar: mientras que los ganchos de WordPress permiten pasar un número ilimitado de parámetros en el gancho y modificar directamente un valor Como respuesta de un filtro, el componente de Symfony requiere instanciar un nuevo objeto para pasar información adicional, y la solución de Laravel sugiere ejecutar un comando en Artisan (CLI de Laravel) para generar los archivos que contienen el evento y los objetos de escucha. Si todo lo que deseamos es modificar algún valor en la aplicación, ejecutar un enlace como $value = apply_filters("modifyValue", $value, $post_id);sea ​​tan simple como sea posible.

En la primera parte de esta serie , expliqué que la aplicación independiente de CMS ya establece una solución particular para la inyección de dependencia en lugar de confiar en la solución del CMS, porque la aplicación en sí misma necesita esta funcionalidad para unir sus partes. Algo similar sucede con los ganchos: son un concepto tan poderoso que la aplicación puede beneficiarse enormemente al ponerla a disposición de los diferentes paquetes independientes de CMS (permitiéndoles interactuar entre sí) y no dejar que este cableado se implemente solo en El nivel de CMS. Por lo tanto, he decidido enviar una solución para el concepto de «gancho» en la aplicación independiente de CMS, y esta solución es la implementada por WordPress.

Para desacoplar los enlaces independientes de CMS de los de WordPress, una vez más debemos «codificar contra interfaces, no implementaciones»: definimos un contrato con las funciones de enlace correspondientes:

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;
}

Tenga en cuenta que las funciones applyFiltersdoActionson variables, es decir, pueden recibir una cantidad variable de argumentos a través del parámetro ...$args. Al combinar esta característica (que se agregó a PHP en la versión 5.6, por lo tanto, no estaba disponible para WordPress hasta hace muy poco tiempo) con el desempaquetado de argumentos, es decir, pasar una cantidad variable de parámetros ...$argsa una función, podemos proporcionar fácilmente la implementación de 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);
  }
}

En cuanto a una aplicación que se ejecuta en Symfony o Laravel, este contrato puede cumplirse instalando un paquete independiente de CMS que implemente ganchos similares a WordPress.

Finalmente, cada vez que necesitamos ejecutar un enlace, lo hacemos a través del servicio correspondiente:

$hooksAPIService = ContainerBuilderFactory::getInstance()->get('hooks_api');
$title = $hooksAPIService->applyFilters("modifyTitle", $title, $post_id);
Abstrayendo los ganchos mismos

Debemos asegurarnos de que, cada vez que se ejecute un enlace, se ejecute una acción coherente sin importar cuál sea el CMS. Para los ganchos definidos dentro de nuestra aplicación, eso no es un problema, ya que podemos resolverlos nosotros mismos, muy probablemente en nuestro paquete independiente de CMS. Sin embargo, cuando el CMS proporciona el enlace, como la acción "init"(activada cuando el sistema se ha inicializado) o el filtro "the_title"(activado para modificar el título de una publicación) en WordPress, e invocamos estos enlaces, debemos asegurarnos de que todos los demás CMS los procesará correcta y consistentemente. (Tenga en cuenta que esto se refiere a ganchos que tienen sentido en cada CMS, como "init"; ciertos otros ganchos pueden considerarse demasiado específicos para WordPress, como el filtro "rest_{$this->post_type}_query"de un controlador REST, por lo que no necesitamos abstraerlos).

La solución que encontré es conectar las acciones o filtros definidos exclusivamente en la aplicación (es decir, no en el CMS), y pasar de los ganchos del CMS a los ganchos de la aplicación cuando sea necesario. Por ejemplo, en lugar de agregar una acción para hook "init"(como se define en WordPress), cualquier código en nuestra aplicación debe agregar una acción en hook "cms:init", y luego implementamos el puente en el paquete específico de WordPress de "init""cms:init":

$hooksAPIService->addAction('init', function() use($hooksAPIService) {
  $hooksAPIService->doAction('cms:init');
});

Finalmente, la aplicación puede agregar un nombre de «contrato suelto» "cms:init"y el paquete específico de CMS debe implementarlo (como se demostró anteriormente).

ENRUTAMIENTO

Los diferentes marcos proporcionarán diferentes soluciones para el enrutamiento (es decir, el mecanismo de identificación de cómo la aplicación manejará la URL solicitada), que reflejan la arquitectura del marco:

  • En WordPress, las URL se asignan a consultas de bases de datos , no a rutas.
  • Symfony proporciona un componente de enrutamiento que es independiente (cualquier aplicación PHP puede instalarlo y usarlo), y que permite definir rutas personalizadas y qué controlador las procesará.
  • El enrutamiento de Laravel se basa en el componente de enrutamiento de Symfony para adaptarlo al marco de Laravel.

Como se puede ver, la solución de WordPress es la atípica aquí: el concepto de mapeo de URL a consultas de bases de datos está estrechamente relacionado con la arquitectura de WordPress, y no queremos restringir nuestra aplicación abstracta a esta metodología (por ejemplo, se puede configurar CMS de octubre -up como un CMS de archivo plano, en cuyo caso no utiliza una base de datos). En cambio, tiene más sentido utilizar el enfoque de Symfony como su comportamiento predeterminado, y permitir que WordPress anule este comportamiento con su propio mecanismo de enrutamiento.

(De hecho, si bien el enfoque de WordPress funciona bien para recuperar contenido, es bastante inapropiado cuando necesitamos acceder a alguna funcionalidad, como mostrar un formulario de contacto. En este caso, antes del lanzamiento de Gutenberg, nos vimos obligados a crear una página y agregar un código corto "[contact_form]"como contenido, que no es tan limpio como simplemente mapear la ruta a su controlador correspondiente directamente).

Por lo tanto, el enrutamiento para nuestra aplicación abstraída no se basará en las entidades modeladas (publicación, página, categoría, etiqueta, autor) sino únicamente en rutas personalizadas. Esto ya debería funcionar perfectamente para Symfony y Laravel, utilizando sus propias soluciones, y no tenemos mucho que hacer aparte de inyectar las rutas con los controladores correspondientes en la configuración de la aplicación.

Sin embargo, para que funcione en WordPress, debemos seguir algunos pasos adicionales: debemos introducir una biblioteca externa para manejar el enrutamiento, como Cortex . Haciendo uso de Cortex, la aplicación que se ejecuta en WordPress puede tener ambas formas:

  • Si hay una ruta personalizada que coincida con la URL solicitada, utilice su controlador correspondiente.
  • si no, deje que WordPress maneje la solicitud a su manera (es decir, recuperando la entidad de la base de datos coincidente o devolviendo un 404 si no hay una coincidencia exitosa).

Para implementar esta funcionalidad, he diseñado el contrato CMSRoutingInterfacepara, dada la URL solicitada, calcular dos datos:

  • la ruta real, como contactpostsposts/my-first-post.
  • la naturaleza de la ruta: valores de la base de la naturaleza "standard""home""404", y los valores de la naturaleza adicionales añadió a través de paquetes, tales como "post"a través de un paquete de “Mensajes” o "user"a través de un paquete de “Usuarios”.

La naturaleza de la ruta es una construcción artificial que permite a la aplicación independiente de CMS identificar si la ruta tiene cualidades adicionales asociadas. Por ejemplo, al solicitar la URL para una sola publicación en WordPress, la publicación del objeto de la base de datos correspondiente se carga en el estado global, en global $post. También ayuda a identificar qué caso queremos manejar, para evitar inconsistencias. Por ejemplo, podríamos haber definido una ruta personalizada contactmanejada por un controlador, que tendrá naturaleza "standard", y también una página en WordPress con slug "contact", que tendrá naturaleza "page"(agregada a través de un paquete llamado «Páginas»). Luego, nuestra aplicación puede priorizar qué forma manejar la solicitud, ya sea a través del controlador o mediante una consulta a la base de datos.

Vamos a implementarlo. Primero definimos el contrato del servicio:

interface CMSRoutingInterface
{
  public function getNature();
  public function getRoute();
}

Luego podemos definir una clase abstracta que proporcione una implementación básica de estas funciones:

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, '/');
  }
}

Y la implementación se anula para 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
    );
  }
}

En el código anterior, tenga en cuenta qué tan constante ROUTE_QUERYes utilizado por el servicio para saber si la ruta es personalizada, como se configura a través de 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;
        }
      ));
    }
  }
);

Finalmente, agregamos nuestras rutas a través del gancho "routes":

$hooksAPIService->addFilter(
  'routes',
  function($routes) {
    return array_merge(
      $routes,
      [
        'contact',
        'posts',
      ]
    );
  }
);

Ahora, la aplicación puede descubrir la ruta y su naturaleza, y proceder en consecuencia (por ejemplo, para que una "standard"naturaleza invoque a su controlador, o para que una "post"naturaleza invoque el sistema de plantillas de WordPress):

$cmsRoutingService = ContainerBuilderFactory::getInstance()->get('routing');
$nature = $cmsRoutingService->getNature();
$route = $cmsRoutingService->getRoute();
// Process the requested route, as appropriate
// ...

PROPIEDADES DEL OBJETO

Una consecuencia bastante inconveniente de abstraer nuestro código es que no podemos hacer referencia a las propiedades de un objeto directamente, y debemos hacerlo a través de una función. Esto se debe a que diferentes CMS representarán el mismo objeto que contiene diferentes propiedades, y es más fácil abstraer una función para acceder a las propiedades del objeto que abstraer el objeto en sí (en cuyo caso, entre otras desventajas, es posible que tengamos que reproducir el objeto mecanismo de almacenamiento en caché del CMS). Por ejemplo, un objeto de publicación $postcontiene su ID $post->IDen WordPress y $post->iden octubre CMS. Para resolver esta propiedad, nuestro contrato PostObjectPropertyResolverInterfacecontendrá la función getId:

interface PostObjectPropertyResolverInterface {
  public function getId($post);
}

Lo que se resuelve para WordPress así:

class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {
  public function getId($post)
  {
    return $post->ID;
  }
}

Del mismo modo, la propiedad de contenido de publicación está $post->post_contenten WordPress y $post->contenten octubre CMS. Nuestro contrato permitirá acceder a esta propiedad a través de la función getContent:

interface PostObjectPropertyResolverInterface {
  public function getContent($post);
}

Lo que se resuelve para WordPress así:

class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {
  public function getContent($post)
  {
    return $post->post_content;
  }
}

Tenga en cuenta que la función getContentrecibe el objeto en sí a través del parámetro $post. Esto se debe a que estamos asumiendo que el contenido será una propiedad del objeto de publicación en todos los CMS. Sin embargo, debemos ser cautelosos al hacer esta suposición y decidir una propiedad por propiedad. Si no queremos hacer la suposición anterior, entonces tiene más sentido que la función getContentreciba la ID de la publicación:

interface PostObjectPropertyResolverInterface {
  public function getContent($post_id);
}

Al ser más conservadora, la firma de la última función hace que el código sea potencialmente más reutilizable, sin embargo, también es menos eficiente, porque la implementación aún tendrá que recuperar el objeto de publicación:

class WPPostObjectPropertyResolver implements PostObjectPropertyResolverInterface {
  public function getContent($post_id)
  {
    $post = get_post($post_id);
    return $post->post_content;
  }
}

Además, algunas propiedades pueden ser necesarias en su valor original y también después de aplicar algún procesamiento; para estos casos, necesitaremos implementar una función adicional correspondiente en nuestro contrato. Por ejemplo, se debe acceder al contenido de la publicación también como HTML, que se realiza mediante la ejecución apply_filters('the_content', $post->post_content)en WordPress o directamente a través de la propiedad $post->content_htmlen el CMS de octubre. Por lo tanto, nuestro contrato puede tener 2 funciones para resolver la propiedad de contenido:

interface PostObjectPropertyResolverInterface {
  public function getContent($post_id); // = raw content
  public function getHTMLContent($post_id);
}

También debemos preocuparnos por abstraer el valor que puede tener la propiedad. Por ejemplo, se aprueba un comentario en WordPress si su propiedad comment_approvedtiene el valor "1". Sin embargo, otros CMS pueden tener una propiedad similar con valor true. Por lo tanto, el contrato debe eliminar cualquier posible inconsistencia o ambigüedad:

interface CommentObjectPropertyResolverInterface {
  public function isApproved($comment);
}

Que se implementa para WordPress de esta manera:

class WPCommentObjectPropertyResolver implements CommentObjectPropertyResolverInterface {
  public function isApproved($comment)
  {
    return $comment->comment_approved == "1";
  }
}

ESTADO GLOBAL

WordPress establece varias variables en el contexto global, como global $postcuando se consulta una sola publicación. Mantener las variables en el contexto global se considera un antipatrón, ya que el desarrollador podría anular involuntariamente sus valores, produciendo errores que son difíciles de rastrear. Por lo tanto, abstraer nuestro código nos da la oportunidad de implementar una mejor solución.

Un enfoque que podemos tomar es crear una clase correspondiente AppStateque simplemente contenga una propiedad para almacenar todas las variables que necesitará nuestra aplicación. Además de inicializar todas las variables principales, permitimos que los componentes inicialicen sus propias variables a través de enlaces:

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;
  }
}

Para reemplazar global $post, un gancho de WordPress puede establecer estos datos a través de un gancho. Un primer paso sería establecer los datos en "post-id":

$hooksAPIService->addFilter(
  "AppState:init", 
  function($vars) {
    if (is_single()) {
      global $post;
      $vars['post-id'] => $post->ID;
    }
    return $vars;
  }
);

Sin embargo, también podemos abstraer las variables globales: en lugar de tratar con entidades fijas (como publicaciones, usuarios, comentarios, etc.), podemos tratar con la entidad de forma genérica "object-id", y obtenemos sus propiedades indagando la naturaleza de la ruta solicitada:

$hooksAPIService->addFilter(
  "AppState:init", 
  function($vars) {
    if ($vars['nature'] == 'post') {
      global $post;
      $vars['object-id'] => $post->ID;
    }
    return $vars;
  }
);

A partir de ahora, si necesitamos mostrar una propiedad de la publicación actual, accedemos a ella desde la clase recién definida en lugar del contexto global:

$vars = AppState::getVars();
$object_id = $vars['object-id'];
// Do something with it
// ...

MODELOS DE ENTIDAD (META, TIPOS DE PUBLICACIÓN, PÁGINAS QUE SON PUBLICACIONES Y TAXONOMÍAS —ETIQUETAS Y CATEGORÍAS—)

Debemos abstraer esas decisiones tomadas para WordPress con respecto a cómo se modelan sus entidades. Siempre que consideremos que la opinión de WordPress también tiene sentido en un contexto genérico, podemos replicar tal decisión para nuestro código independiente de CMS.

Meta:

Como se mencionó anteriormente, el concepto de «meta» se debe desacoplar de la entidad modelo (como «post meta» de «posts»), por lo que si un CMS no proporciona soporte para meta, puede descartar solo esta funcionalidad.

Luego, el paquete «Post Meta» (desacoplado del paquete «Posts», pero dependiente de él) define el siguiente contrato:

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);
}

Lo que se resuelve para WordPress así:

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);
  }
}
Tipos de publicaciones:

He decidido que el concepto de WordPress de un tipo de publicación personalizado, que permite modelar entidades (como un evento o una cartera) como extensiones de publicaciones, puede aplicarse en un contexto genérico, y como tal, he replicado esta funcionalidad en el CMS -código de diagnóstico. Esta decisión es controvertida, sin embargo, lo justifico porque la aplicación puede necesitar mostrar una fuente de entradas de diferentes tipos (como publicaciones, eventos, etc.) y los tipos de publicaciones personalizadas hacen posible dicha implementación. Sin tipos de publicaciones personalizadas, esperaría que la aplicación necesitara ejecutar varias consultas para traer los datos para cada tipo de entidad, y la lógica se complicaría (por ejemplo, si se obtienen 12 entradas, deberíamos obtener 6 publicaciones y 6 eventos) ? pero qué pasa si los eventos se publicaron mucho antes que las últimas 12 publicaciones? y así sucesivamente).

¿Qué sucede cuando el CMS no admite este concepto? Bueno, no sucede nada grave: una publicación aún indicará que su tipo de publicación personalizada es una «publicación», y ninguna otra entidad heredará de la publicación. La aplicación seguirá funcionando correctamente, solo con una ligera sobrecarga del código innecesario. Esta es una compensación que, creo, vale más que la pena.

Para admitir tipos de publicaciones personalizadas, simplemente agregamos una función getPostTypeen nuestro contrato:

interface PostAPIInterface
{
  public function getPostType($post_id);
}

Lo que se resuelve para WordPress así:

class WPPostAPI implements PostAPIInterface
{
  public function getPostType($post_id) {
    return get_post_type($post_id);
  }
}
Páginas que se publican:

Si bien justifico mantener tipos de publicaciones personalizadas para extender las publicaciones, no justifico que una página sea una publicación, como sucede en WordPress, porque en otros CMS estas entidades están completamente desacopladas y, lo que es más importante, una página puede tener un rango más alto que una publicación, por lo que hacer que una página se extienda desde una publicación no tendría sentido. Por ejemplo, October CMS incluye páginas en su funcionalidad principal, pero las publicaciones deben instalarse a través de complementos.

Por lo tanto, debemos crear contratos separados para publicaciones y páginas, aunque puedan contener las mismas funciones:

interface PostAPIInterface
{
  public function getTitle($post_id);
}

interface PageAPIInterface
{
  public function getTitle($page_id);
}

Para resolver estos contratos para WordPress y evitar la duplicación de código, podemos implementar la funcionalidad común a través de un rasgo:

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;
}
Taxonomías (etiquetas y categorías):

Una vez más, no podemos esperar que todos los CMS admitan lo que se llama taxonomías en WordPress: etiquetas y categorías. Por lo tanto, debemos implementar esta funcionalidad a través de un paquete de «Taxonomías» y, suponiendo que se agreguen etiquetas y categorías a las publicaciones, este paquete dependerá de las «Publicaciones» del paquete.

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);
  ...
}

Podríamos haber decidido crear dos paquetes separados «Categorías» y «Etiquetas» en lugar de «Taxonomías», sin embargo, como la implementación en WordPress lo hace evidente, una etiqueta y una categoría son básicamente el mismo concepto de entidad con solo una pequeña diferencia: las categorías son jerárquicas (es decir, una categoría puede tener una categoría principal), pero las etiquetas no. Entonces, considero que tiene sentido mantener este concepto para un contexto genérico, y enviado bajo un solo paquete «Taxonomías».

Debemos prestar atención a que ciertas funcionalidades involucran tanto publicaciones como taxonomías, y estas deben estar desacopladas adecuadamente. Por ejemplo, en WordPress podemos recuperar publicaciones que fueron etiquetadas "politics"al ejecutarlas get_posts(['tag' => "politics"]). En este caso, si bien la función getPostsdebe implementarse en el paquete “Publicaciones”, el filtrado por etiquetas debe implementarse en el paquete “Taxonomías”. Para lograr esta separación, simplemente podemos ejecutar un enlace en la implementación de la función getPostspara WordPress, permitiendo que cualquier componente modifique los argumentos antes de ejecutar get_posts:

class WPPostAPI implements PostAPIInterface
{
  public function getPosts($args) {
    $args = $hooksAPIService->applyFilters("modifyArgs", $args);
    return get_posts($args);
  }
}

Y finalmente implementamos el gancho en el paquete «Taxonomías para 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;
  }
);

Tenga en cuenta que en el código resumido los atributos se redefinieron (siguiendo las recomendaciones para abstraer los parámetros de la función, explicados anteriormente): "tag"deben proporcionarse como "tags""cat"deben proporcionarse como "categories"(cambiando la connotación del singular al plural), y estos valores deben se pasa como matrices (es decir, se eliminan aceptando cadenas separadas por comas como en WordPress, para agregar consistencia).

TRADUCCIÓN

Debido a que las llamadas para traducir cadenas se extienden por todo el código de la aplicación, la traducción no es una funcionalidad de la que podemos optar, y debemos asegurarnos de que los otros marcos sean compatibles con nuestro mecanismo de traducción elegido.

En WordPress, que la internacionalización implementos a través de gettext , estamos obligados a archivos de traducción de configuración para cada código de configuración regional (como ‘es_ES’, que es el código para fr lenguaje ench de FR Ance), y estos pueden ser fijados en un texto dominio (que permite que los temas o complementos definan sus propias traducciones sin temor a colisión con las traducciones de otras piezas de código). No necesitamos verificar la compatibilidad de los marcadores de posición en la cadena para traducir (como cuando se hace sprintf(__("Welcome %s"), $user_name)), porque la función sprintfpertenece a PHP y no al CMS, por lo que siempre funcionará.

Verifiquemos si los otros marcos admiten las dos propiedades requeridas, es decir, obtener los datos de traducción para un entorno local específico compuesto por idioma y país, y bajo un dominio de texto específico:

  • El componente de traducción de Symfony admite estas dos propiedades.
  • La configuración regional utilizada en la localización de Laravel involucra el idioma pero no el país, y los dominios de texto no son compatibles (podrían replicarse mediante la anulación de los archivos de idioma del paquete, pero el dominio no se establece explícitamente, por lo que el contrato y la implementación serían inconsistentes con cada uno otro).

Sin embargo, afortunadamente existe la biblioteca Laravel Gettext que puede reemplazar la implementación nativa de Laravel con el componente de traducción de Symfony. Por lo tanto, obtuvimos soporte para todos los marcos, y podemos confiar en una solución similar a WordPress.

Luego podemos definir nuestro contrato reflejando las firmas de funciones de WordPress:

interface TranslationAPIInterface
{
  public function __($text, $domain = 'default');
  public function _e($text, $domain = 'default');
}

La implementación del contrato para WordPress es así:

class WPTranslationAPI implements TranslationAPIInterface
{
  public function __($text, $domain = 'default')
  {
    return __($text, $domain);
  }
  public function _e($text, $domain = 'default')
  {
    _e($text, $domain);
  }
}

Y para usarlo en nuestra aplicación, hacemos:

$translationAPI = ContainerBuilderFactory::getInstance()->get('translation_api');
$text = $translationAPI->__("translate this", "my-domain");

MEDIOS DE COMUNICACIÓN

WordPress tiene la administración de medios como parte de su funcionalidad principal, que representa un elemento de medios como una entidad por sí mismo, y permite manipular el elemento de medios (como recortar o cambiar el tamaño de las imágenes), pero no podemos esperar que todos los CMS tengan un aspecto similar funcionalidad Por lo tanto, la gestión de medios debe estar desacoplada de la funcionalidad central de CMS.

Para el contrato correspondiente, podemos reflejar las funciones de los medios de WordPress, pero eliminando la terquedad de WordPress. Por ejemplo, en WordPress, un elemento multimedia es una publicación (con tipo de publicación "attachment"), pero para el código independiente de CMS no lo es, por lo tanto, el parámetro debe ser $media_id(o $image_id) en lugar de $post_id. Del mismo modo, WordPress trata los medios como archivos adjuntos a las publicaciones, pero este no tiene por qué ser el caso en todas partes, por lo tanto, podemos eliminar la palabra «archivo adjunto» de las firmas de funciones. Finalmente, podemos decidir mantener $sizela imagen en el contrato; Si el CMS no admite la creación de múltiples tamaños de imagen para una imagen, entonces puede volver a su valor predeterminado NULLy no sucede nada grave:

interface MediaAPIInterface
{
  public function getImageSrcAndDimensions($image_id, $size = null): array;
  public function getImageURL($image_id, $size = null): string;
}

La respuesta por función también getImageSrcAndDimensionsse puede deducir, devolviendo una matriz de nuestro propio diseño en lugar de simplemente reutilizar el de la función de WordPress 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);
  }
}

Conclusión

Configurar una arquitectura independiente de CMS para nuestra aplicación puede ser una tarea difícil. Como se demostró en este artículo, abstraer todo el código fue un proceso largo, que tomó mucho tiempo y energía para lograrlo, y aún no está terminado. No me sorprendería si el lector se siente intimidado por la idea de pasar por este proceso para convertir una aplicación de WordPress en una aplicación independiente de CMS. Si no hubiera hecho la abstracción yo mismo, ciertamente también me sentiría intimidado.

Mi sugerencia es que el lector analice si pasar por este proceso tiene sentido según cada proyecto. Si no hay necesidad de portar una aplicación a un CMS diferente, entonces tendrá derecho a mantenerse alejado de este proceso y apegarse a la forma de WordPress. Sin embargo, si necesita migrar una aplicación fuera de WordPress y desea reducir el esfuerzo requerido, o si ya necesita mantener varias bases de código que se beneficiarían de la reutilización del código, o incluso si puede migrar la aplicación en algún momento en el futuro y acaba de comenzar un nuevo proyecto, entonces este proceso es para usted. Puede ser doloroso implementarlo, pero vale la pena. Lo sé porque he estado allí. Pero he sobrevivido, y ciertamente lo volvería a hacer. Gracias por leer.

Deja un comentario

Tu dirección de correo electrónico no será publicada.