J4 - Porque o Joomla deve fazer o Routing Associações de linguagens por nós
J4 - Porque o Joomla deve fazer o Routing Associações de linguagens por nós
Hugo Azevedo 104

J4 - Porque o Joomla deve fazer o Routing e Associações de linguagens por nós

Nota: Este documento faz parte duma série de documentos que constituem um ensaio sobre o que o Joomla! é, e poderá ser no futuro. Estes documentos não representam o estado actual de desenvolvimento do Joomla!, nem são apenas "despejos de memória". Eles listam um conjunto de ideias estruturadas sobre o que o Joomla! poderá ser, e apresentam algumas soluções para lá chegar. O seu maior propósito é gerar discussão na comunidade do Joomla!.

Quem já desenvolveu um componente no Joomla!, sabe que existe muito código repetido numa nova extensão. Routing e Associações de Linguages são bons exemplos disto.

Routing

Routing tem um papel importante em componentes MVC, numa aplicação que usa URLs SEF. Não é necessário se usarmos apenas query strings nos URLs.

Pode adoptar muitos formatos, mais a sua principal função é converter os segmentos num URL SEF, a mapeá-los com um método num Controller de um Componente (ex:. /component/controller/method/record). Um dos seus maiores benefícios, é que pode rotear um URL SEF a um pedido de um registo, sem necessitar duma ligação a uma base de dados. Isto é feito, identificando os vários segmentos do URL que o utilizador quer, e reencaminhar o pedido para um método que irá retornar o resultado pretendido.

No Joomla!, isto é feito de forma um pouco diferente. O Joomla! usa um stub de registo, que é uma solução muito pouco elegante que use o id do registo ao seu alias, de forma a identificar um registo num URL SEF. Mesmo não sendo elegante, é uma forma muito eficiente, de forma a reter o alias do registo, e ao mesmo tempo manter o id do registo, que pode ser retirado facilmente convertendo (casting) o segmento do registo num inteiro.

Implementar routing nos nossos componentes, não é uma tarefa muito pacífica. O Joomla! deixa-nos desenvolver o nosso próprio router no componente, que pode ser implementado com relativa liberdade, mas implementá-lo de qualquer outra forma que não a forma usada nos componentes "principais", irá deixar-nos com vários tipos de URL no nosso site.

Na realidade, os componentes nem sequer deveriam ter de lidar com o routing no Joomla!. Isso é da responsabilidade do próprio Joomla!. Se uma "rota" é configurada no nosso sistema (e deverá ser configurável), todas as extensões deverão ser capazes de a usar, sem ter de a re-implementar novamente, o que irá permitir que o Joomla! possa implementar qualquer tipo de "rotas" que ele queira. Se o Joomla! implementar uma "rota" por defeito, então todas as extensões deverão parecer iguais. Alguns exemplos de "rotas" podem ser:

Nestes exemplos, a única coisa que o Joomla! poderá precisar, é um alias único de registo de último segmento, e daí, deverá poder encontrar o método do Controller que deverá executar. Isto quer dizer que, os alias dos registos deverão ser todos centralizados.

Mas de forma a conseguir isto, talvez o Joomla! devesse usar mapeamento de registos, em prol de routers individuais.

Associações de Linguages

Por defeito, o Joomla! é um CMS multi-linguagem, o que é muito bom. Mesmo que o nosso site tenha apenas uma linguagem inicial, continua a ser um site multi-linguagem. Não porque permite ter várias linguagens em simultâneo, mas porque permite definir uma linguagem por defeito diferente, duma listagem de várias linguagens disponíveis, e porque nos permite traduzir qualquer campo/output que produz.

Um dos maiores problemas que tenho com esta abordagem, é porque tenho eu sempre de desenvolver o mesmo código, cada vez que quero tirar partido disso, logo, tendo de manter cada vez que haja uma alteração do lado do Joomla!. Outro aspecto é, o que constitui na realidade uma tradução de um registo no Joomla!. Poderemos começar por aqui.

O que é um registo?

Então, o que é um registo? No Joomla!, e considerando a forma como este lida com traduções multi-linguagem, um registo/entidade não é na realidade um registo. Um registo é na realidade um conjunto de registos independentes, que são associados pelo utilizador. Esta abordagem cria um conjunto de problemas que uma verdadeira tradução de registos não tem. Vamos ser mais específicos.

Quando criamos um Artigo no Joomla!, nós escolhemos a linguagem principal para esse mesmo Artigo. Se necessitarmos duma tradução desse Artigo, nós criamos um segundo artigo independente, e associamos os dois no final. O problema é que, embora ambos falem da mesma coisa em linguagens diferentes, eles são registos independentes, e não traduções.

Joomla! tem um conjunto de campos pré-definidos na base de dados que mantêm informação sobre um registo:

Agora, vamos ver este exemplo simples. Imaginemos que temos umas centenas de Artigos (Produtos) criados no site, em 5 ou 6 linguagens diferentes, e queremos saber qual o Produto mais visitado. Como é que o faríamos?

Problemas que esta abordagem cria

Duma perspectiva de UX, existe um grande problema (que podem testar) que sobressai rapidamente. Quando criamos um Artigo pela primeira vez, e mesmo especificando a linguagem inicial do mesmo, o Joomla! ainda não sabe em que linguagem o Artigo está, até o guardarmos. Isto quer dizer que, teremos de o salvar primeiro, antes de o podermos associar a outra linguagem no tabulador de Associações, porque inicialmente, dá-nos a impressão de que poderemos associar Artigos na mesma linguagem, que não é verdade.

Outro problema de UX que existe nesta abordagem, é que teremos de usar o filtro (se disponível) de forma a filtrar registos duma linguagem específica, de forma a encurtar a listagem. Se não o fizermos, estaremos a exibir Artigos repetidos na listagem, fazendo a mesma ser substancialmente maior. Embora não estando bem, na View da listagem isto não constitui um grande problema, apenas teremos de alterar a linguagem no filtro, que será apenas mais uma interacção do utilizador. Mas vamos assumir que já nos encontramos numa tradução específica de um dado Artigo/Produto. Não existe maneira de "saltar" directamente para a tradução duma linguagem específica, sem termos de voltar à listagem anterior e mudar a mesma nos filtros, e procurar pelo Artigo independente certo, e voltar a entrar na View de edição. Agora imaginem este cenário, onde temos várias linguagens, e umas centenas de Artigos...

Se ainda não conseguem ver o problema, imaginem gerir também as respectivas Categorias, além dos Artigos/Produtos/etc... Pensem numa estrutura de Categorias extensa e complexa como a de um site de e-commerce., e tentem definir datas de publicação para Categorias e/ou Produtos, e definir permissões de utilizadores específicas, e mesmo marcá-las como "destacadas"...

Do ponto de vista dos dados, esta abordagem também apresenta uma série de problemas. O que é realmente um Registo? Se queremos saber quando foi criado um determinado conteúdo, quantas visitas teve, quem o criou inicialmente, como é que sabemos? A verdade é que, qualquer que seja a nossa abordagem, não irá ser uma abordagem directa. Teremos sempre de contornar o Joomla!.

Outro problema referente aos dados, é que os registos (associações), não estão mesmo ligados na base de dados. Os dados de associações estão guardados na base de dados, mas a associação propriamente dita, é feita do lado da aplicação, o que será sempre mais lento.

Alterar associações de linguagem para tradição de registos

De forma a conseguir isto, o Joomla! necessita tratar um registo como um registo singular. Esse mesmo registo tem propriedades comuns que propagam para qualquer tradução que este possa ter. Ele tem um ID, pertence a uma Categoria, tem uma data de Criação e uma de Modificação, tem um Criador e um utilizador que fez a última alteração no mesmo, até tem mesmo um controlo de edição exclusiva (check-in). E tudo isto deverá ser comum a toda e qualquer extensão. Deve ser tratado directamente pelo Joomla!.

Aqui está uma possível estrutura para a tabela de registos (conteúdos):

CREATE TABLE `record` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Record ID',
  `catid` int(11) NOT NULL COMMENT 'Category ID',
  `created` datetime NOT NULL COMMENT 'Date the record was created',
  `created_by` int(11) NOT NULL DEFAULT '0' COMMENT 'Creators user ID',
  `modified` datetime NOT NULL COMMENT 'Date the record was last modified',
  `modified_by` int(11) NOT NULL DEFAULT '0' COMMENT 'Last modification user ID',
  `checked_out` int(11) DEFAULT NULL COMMENT 'The ID of the user that checked out the record',
  `checked_out_time` datetime DEFAULT NULL COMMENT 'The date the record was checked out',
  `state` int(11) NOT NULL DEFAULT '1' COMMENT 'The Record state',
  `publish_up` datetime DEFAULT NULL COMMENT 'Publication statring date',
  `publish_down` datetime DEFAULT NULL COMMENT 'Publication ending date',
  `ordering` int(11) NOT NULL DEFAULT '1' COMMENT 'The record order in a list',
  `hits` int(11) NOT NULL DEFAULT '0' COMMENT 'The record hit counter',
  `featured` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'If the record is a featured record',
  `extension` varchar(45) NOT NULL DEFAULT 'com_content' COMMENT 'The extension that will process the record',
  `task` varchar(45) NOT NULL DEFAULT 'controller.task' COMMENT 'The task that will execute to display this record',
  PRIMARY KEY (`id`) COMMENT 'Table primary key',
  CONSTRAINT `record_category`
    FOREIGN KEY (`catid`) REFERENCES `categories` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Como podem ver, a tabela de registos nem tem um Título e/ou Alias, mas também porque haveria de ter?! Títulos e Alias são específicos de linguagens, e neste exemplo, um registo não necessita saber, ou preocupar-se com qualquer linguagem. Deverá apenas preocupar-se com a identificação e catalogação do conteúdo.

Então, como iria o Joomla! distinguir o conteúdo das várias Extensões, se todas usam a mesma tabela de conteúdos? Bom, é qui que as últimas duas colunas são relevantes. As últimas duas colunas são extension e task. Se lerem o post anterior sobre J4 - Porque o Joomla! precisa duma Framework e duma Plataforma, e o post J4 - Como pode o Joomla! alterar e melhorar o seu padrão MVC, então a perceber como funcionaria.

Um registo terá um método dum Controller associado, que irá controlar como este é exibido. E um Controller pertence a uma Extensão. O utilizador pode definir o método correcto a usar, duma lista predefinida de métodos disponíveis, da mesma forma que ele(a) define uma Categoria para o registo.

Para as Traduções, teremos de ter uma segunda tabela que as irá guardar. A estrutura poderá p+arecer-se com a seguinte:

For Translations, you would have a secondary table that would handle them. The structure could be something like this:

CREATE TABLE `translations` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Translation ID',
  `recid` int(11) NOT NULL COMMENT 'The Record id',
  `language` varchar(10) NOT NULL COMMENT 'The language code for the Translation',
  `state` int(11) NOT NULL DEFAULT '0' COMMENT 'The Translation state',
  `title` varchar(250) NOT NULL COMMENT 'The title for the Translation',
  `alias` varchar(250) NOT NULL COMMENT 'The alias for the Translation',
  PRIMARY KEY (`id`),
  UNIQUE KEY `translation_unique_lang` (`recid`,`language`) COMMENT 'Single translation per record',
  UNIQUE KEY `translation_unique_alias` (`language`,`alias`) COMMENT 'Single alias per language',
  CONSTRAINT `translation_record`
    FOREIGN KEY (`recid`) REFERENCES `record` (`id`)
    ON DELETE CASCADE
    ON UPDATE CASCADE,
  CONSTRAINT `translation_language`
    FOREIGN KEY (`language`) REFERENCES `languages` (`language`)
    ON DELETE CASCADE
    ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Como se pode ver, esta tabela secundária necessita de referenciar a tabela de registos. Cada registo de tradução, tem necessariamente de estar ligado a um registo. Não faz sentido traduzir algo que não existe. Esta tradução será reforçada com um FOREIGN KEY à tabela de registos.

Como protecção, e como forma de melhorar o desempenho de queries SELECT, um segundo índice único deve ser criado para incluir o ID do registo e da linguagem, trancando cada possívelcombinação dos dois. O sistema não deverá ter duas traduções para a mesma linguagem.

Se informação sobre o registo de tradução for necessária, algumas das colunas existentes na tabela de registos poderão também existir na tabela de traduções. Exemplos disto poderão ser as colunas hits, created e modified. que poderão também existir na tabela de traduções, tal como o state também existe.

Então, como deve um programador definir uma tabela para a sua extensão?

Da mesma forma que hoje em dia um programador adiciona a coluna catid, ele/a agora irá apenas adicionar uma coluna id, que irá ligar à tradução.

Vamos pensar numa extensão que faz a gestão de produtos, que irá guardar 3 propriedades desses mesmo produtos, preço, descrição e foto. Hoje em dia, a tabela dessa extensão poderá parecer-se com a seguinte definição:

CREATE TABLE `joomla`.`product` (
    `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Product ID',
    `catid` INT NOT NULL COMMENT 'Category ID',
    `created` DATETIME NOT NULL COMMENT 'Date the product was created',
    `created_by` INT NOT NULL COMMENT 'Creator ID',
    `modified` DATETIME NOT NULL COMMENT 'Date the product was last modified',
    `modified_by` INT NOT NULL COMMENT 'Last modification user ID',
    `checked_out` INT NULL DEFAULT NULL COMMENT 'The ID of the user that checked out the product',
    `checked_out_time` DATETIME NULL DEFAULT NULL COMMENT 'The date the product was checked out',
    `state` INT NOT NULL COMMENT 'The products state',
    `publish_up` DATETIME NULL DEFAULT NULL COMMENT 'Publication starting date',
    `publish_down` DATETIME NULL DEFAULT NULL COMMENT 'Publication ending date',
    `ordering` INT NOT NULL DEFAULT 1 COMMENT 'The product ordering',
    `hits` INT NOT NULL DEFAULT 0 COMMENT 'The product hit counter',
    `featured` TINYINT NOT NULL DEFAULT 0 COMMENT 'If the product is a featured product',
    `price` DOUBLE NOT NULL DEFAULT 0 COMMENT 'Products price',
    `description` TEXT NOT NULL COMMENT 'Products description',
    `image` VARCHAR(45) NOT NULL COMMENT 'Products image',
PRIMARY KEY (`id`)  COMMENT 'Tables primary key');

Se o programador não seguir esta estrutura de tabela, ele/a não irá tirar partido de todas as funcionalidades que o Joomla! tem disponíveis.

Com a nova estrutura, o programador apenas terá de criar uma tabela com a seguinte estrutura:

CREATE TABLE `joomla`.`product` (
    `id` INT NOT NULL COMMENT 'Translation ID',
    `price` DOUBLE NOT NULL DEFAULT 0 COMMENT 'Product price',
    `description` TEXT NOT NULL COMMENT 'Product description',
    `image` VARCHAR(45) NOT NULL COMMENT 'Product image',
    KEY `product_index` (`id`),
    CONSTRAINT `product_translation`
        FOREIGN KEY (`id`) REFERENCES `translations` (`id`)
        ON DELETE CASCADE
        ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Porque todos os outros campos estão nas tabelas de conteúdos do Joomla!, o/a programador(a) não teria de se preocupar com a sua gestão. Não só irá a sua manutenção ser mais facilitada, como também irá dar ao Joomla! mais liberdade para desenvolver sobre as tabelas base, sem problemas de compatibilidade.

Como podem ver, esta última tabela nem sequer tem definida uma Chave Primária, apenas um Índice Único e uma Chave Externa. O ID do Produto irá referenciar o ID da tradução, e neste sentido, a tabela da extensão apenas teria os campos que as tabelas base ainda não tenham.

Isto ainda permitiria ao programador(a) ter qualquer tipo de estrutura que necessite na tabela. Na realidade, em vez de necessitar de replicar a mesma estrutura cada vez que cria uma nova tabela, ele/a teria apenas de definir o Índice Único e a Chave Externa numa única coluna.

Isto também iria facilitar o trabalho do Joomla! ao tentar identificar o que o cliente está a pedir. A seguinte query string '?option=ecommerce&view=product&layout=edit&format=html&&lang=en-US&id=4' ficaría apenas '?lang=en-US&id=4', porque todas as outras informações já existiriam na tabela de registos. Se mais funcionalidade fosse necessária, a variável task seria usada. O ID fornecido seria o ID de registo, não o ID de Produto. A linguagem do utilizador irá fornecer a linguagem da tradução, e daí, conseguimos o resto da informação do Produto.

Testar a solução

Primeiro, vamos criar uma tabela de Categorias, com apenas duas colunas (id, nome), de forma a poder ligar uma FK (Foreign Key) à tabela de registos. Inseri 1k (1000) categorias na tabela, que me parece ser um site de tamanho razoável.

Depois populei a tabela de registos, com groupos de 1k desordenados aleatoriamente (SELECT INTO) até ter 1M de registos, que me pareceu que fosse um site de tamanho bastante respeitável. Não nos podemos esquecer que a tabela de registos irá guardar todos os registos no sistema, independentemente da extensão a que o registo pertença. Acabei com uma tabela de 110MB em disco.

Após ter criado a tabela de registos,  precisava criar a tabela de traduções, mas antes disso, necessitava da tabela de linguagens para que pudesse ter uma FK para as traduções. Adicionei 6 linguagens distintas, e depois usei-as para adicionar aleatoriamente todos os registos da tabela principal, na tabela de traduções para cada linguagem. Isto criou-me 6M de registos desordenados, e uma tabela com mais de 1GB.

Acabei criando uma tabela para os Produtos, e inseri-lhe 100k produtos ordenados aleatoriamente (das traduções). Num sistema de 1M de registos, e 6 linguagens distintas, assumi que 100k seriam registos de Produtos.

Todas as tabelas estariam a usar o motor MySQL INNODB para serem compatíveis com ACID, que as torna mais lentas.

Também criei uma tabela MyISAM, sem chaves externas, com a actual estrutura do Joomla!, e contendo a mesma informação. Aqui estão os resultados:

Command executed. 1st MyISAM Min. MyISAM 1st INNODB Min. INNODB
SELECT com pagnação simles (10 resultados, sem índice) 0,041s 0,012s 0,211s 0,134s
SELECT filtrado por data 0,00080s 0,00025s 0,0089s 0,0015s
SELECT filtrado por alias (registo único) 0,025s 0,025s 0,142s 0,106s
UPDATE alterar preço do produto 0,00019s 0,00019s 0,0064s 0,0064s
UPDATE alterar título do produto 0,00035s 0,00020s 0,0064s 0,0057s
UPDATE alterar a data de alteração do produto 0,00055s 0,00020s 0,0064s 0,0055s
INSERT novo produto 0,00013s 0,00011s 0,0088s 0,0053s

Como esperado, MyISAM tem os melhores resultados, mas a finalidade deste exercício, era mostrar que uma solução que distribui a informação de um 'Registo' por 3 tabelas, usando um motor compatível com ACID, é viável. Correr as tabelas principais do Joomla! em INNODB, irá fazer o sistema muito mais fiável.

Beneficios desta abordagem

Existem vários benefícios nesta abordagem. Para começar, qualquer conteúdo criado no Joomla!, teria apenas um registo com um ID único, independentemente da extensão à qual o registo pertence.

Se as tabelas Registos e Traduções fizerem parte integral do Joomla!, então ele pode tratar esses mesmos dados pelo/a programador(a). Os Models iriam tratar esses dados pelo/a programador(a). O/A programador(a) apenas teria de se preocupar com os campos de tabela referentes apenas à sua extensão.

O roteamento das extensões não seria necessário. Roteamento seria centralizado também, logo, seria uniforme a todo o site, independentemente de quantas extensões o Utilizador/Implementador tem. Apenas um Router central seria necessário, e poderia ter qualquer formato.

Roteamento centralizado tornaria a infame variável ItemID obsoleta, dentro e fora da query string. O Joomla! não estaria dependente de Items de Menu para roteamento, nem para URLs SEF. A tabela de traduções iria forçar alias únicos, e ligando à tabela de registos, o Joomla! saberia qual a extensão e/ou método invocar. URLs como /component/extension/ acabariam...

Carregamento de Módulos também não necessitaria de Items de Menu. Módulos seriam carregados dependendo da extensão principal usada (contexto), que faz muito mais sentido.

Associações de linguagem deixariam de existir por completo. Traduções de registos estariam ligadas automaticamente, devido ao facto de que elas apontam para o mesmo registo. Isto quer dizer que o tabulador de Associações no UI desapareceria, e os/as programadores(as) de extensões não teriam de se preocupar com código replicado nas suas classes.

Do ponto de vista do UX, também existem benefícios. Na View de listagem (ex:. articles) o utilizador apenas veria os registos na linguagem que escolheu no UI, ou na linguagem por defeito se a primeira não for encontrada. A verdade é que, o/a utilizador(a) não teria de filtrar a listagem por linguagem. Não haveria propósito para isso.

Ao editar um registo, e seleccionar uma nova linguagem na lista drop down, o/a utilizador(a) apenas teria de inserir a informação, e o Joomla! iria criar/editar os registos correspondentes e suas traduções. Os registos (ex:. Articles/Products) estariam ligados automaticamente, porque funcionariam apenas como traduções para o registo na tabela principal.

E por fim, a query string no Joomla! estaria muito mais limpa. Só necessitaria da variável da linguagem (que poderá vir da sessão), e do ID. A partir daí, o Joomla! pode carregar a combinação correcta Controller->Method, que irá retornar a View que o/a utilizador(a) pretende, no formato correcto, parcial ou total, etc...

SmarterQueue - A forma mais inteligente de criar em redes sociais

SmarterQueue - A forma mais inteligente de criar em redes sociais

LRTT - Limited Resource Teacher Training

LRTT - Limited Resource Teacher Training

shareOptic - Solução de monitorização de cyber segurança

shareOptic - Solução de monitorização de cyber segurança

Blockz RAD framework web em Php

Blockz RAD framework web em Php