11.

Navegador de contenidos

|

Share this awesome video!

|

Ahora podemos incrustar listas, cuadrículas o galerías de recetas en miniatura en cualquier diseño de forma dinámica. ¡Es genial! Y siempre podemos crear más tipos de consulta para, por ejemplo, elegir entre las últimas recetas o las recetas más populares.

Pero, ¿y si pudiéramos seleccionar recetas manualmente? Quizá queramos destacar cuatro recetas concretas en la página de inicio. En el área Diseños, en la rejilla, si cambiamos el "Tipo de colección", podemos cambiar a "Colección manual". Pero entonces... en realidad no podemos seleccionar ningún elemento.

Activar elementos manuales en la configuración

Para permitir que los elementos (en nuestro caso, las recetas) se seleccionen manualmente, primero tenemos que permitirlo en la configuración. Antes, cuando creamos la configuración value_types, pusimos manual_items en false. Cámbialo a true:

36 lines | config/packages/netgen_layouts.yaml
netgen_layouts:
// ... lines 2 - 3
value_types:
doctrine_recipe:
// ... line 6
manual_items: true
// ... lines 8 - 36

Y ahora, cuando intentamos acceder a la página, ¡nos aparece un error!

El backend del Navegador de Contenidos Netgen para el tipo de valor doctrine_recipe no existe.

¡Sí! Necesitamos implementar una clase que ayude a los Layouts a navegar por nuestras recetas. Eso se llama "navegador de contenido".

Configurar el "tipo de elemento" en NetgenContentBrowserBundle

En realidad, añadir un navegador de contenidos se hace mediante un bundle completamente distinto, que puedes utilizar fuera de Netgen Layouts. Es útil si necesitas una interfaz agradable para navegar y seleccionar elementos.

Como el navegador de contenidos se encuentra en un bundle diferente, no es necesario, pero voy a configurarlo con un nuevo archivo de configuración llamado netgen_content_browser.yaml. Dentro, establece la clave raíz en netgen_content_browser para configurar el "NetgenContentBrowserBundle":

netgen_content_browser:
// ... lines 2 - 8

Dentro de éste, podemos describir todas las diferentes "cosas manuales" que queremos poder navegar. Para ello, añade una clave item_types y, para el primer elemento, coge el nombre interno del tipo de valor - doctrine_recipe - para que coincidan, pégalo y dale un nombre. Qué te parece... Recipes con un bonito icono de fresa:

netgen_content_browser:
item_types:
# must match "value_types" key in netgen_layouts config
doctrine_recipe:
name: 'Recipes 🍓'
// ... lines 6 - 8

Lo único que necesitamos aquí es una clave preview con una subclave template, que pondré nglayouts/content_browser/recipe_preview.html.twig:

netgen_content_browser:
item_types:
# must match "value_types" key in netgen_layouts config
doctrine_recipe:
name: 'Recipes 🍓'
preview:
template: 'nglayouts/content_browser/recipe_preview.html.twig'

Y asegúrate de escribir "plantilla" correctamente. ¡Uy! De todas formas, estamos poniendo este preview.template porque la configuración nos lo exige... pero ya nos preocuparemos de crear esa plantilla más adelante.

Crear la clase backend

Si nos dirigimos y actualizamos... obtenemos el mismo error. Eso es porque necesitamos una clase backend que se conecte a este nuevo tipo de elemento. Crear un backend es un proceso sencillo, pero requiere algunas clases diferentes.

En el directorio src/, vamos a crear un nuevo directorio llamado ContentBrowser/... y dentro de él, una clase PHP llamada RecipeBrowserBackend. Ésta necesita implementar BackendInterface: la de Netgen\ContentBrowser\Backend:

// ... lines 1 - 2
namespace App\ContentBrowser;
use Netgen\ContentBrowser\Backend\BackendInterface;
// ... lines 6 - 8
class RecipeBrowserBackend implements BackendInterface
{
// ... lines 11 - 54
}

A continuación, ve a "Código"->"Generar" (o Command+N en un Mac) para implementar los nueve métodos que necesita No te preocupes: no es tan malo como parece:

// ... lines 1 - 2
namespace App\ContentBrowser;
use Netgen\ContentBrowser\Backend\BackendInterface;
use Netgen\ContentBrowser\Item\ItemInterface;
use Netgen\ContentBrowser\Item\LocationInterface;
class RecipeBrowserBackend implements BackendInterface
{
public function getSections(): iterable
{
// TODO: Implement getSections() method.
}
public function loadLocation($id): LocationInterface
{
// TODO: Implement loadLocation() method.
}
public function loadItem($value): ItemInterface
{
// TODO: Implement loadItem() method.
}
public function getSubLocations(LocationInterface $location): iterable
{
// TODO: Implement getSubLocations() method.
}
public function getSubLocationsCount(LocationInterface $location): int
{
// TODO: Implement getSubLocationsCount() method.
}
public function getSubItems(LocationInterface $location, int $offset = 0, int $limit = 25): iterable
{
// TODO: Implement getSubItems() method.
}
public function getSubItemsCount(LocationInterface $location): int
{
// TODO: Implement getSubItemsCount() method.
}
public function search(string $searchText, int $offset = 0, int $limit = 25): iterable
{
// TODO: Implement search() method.
}
public function searchCount(string $searchText): int
{
// TODO: Implement searchCount() method.
}
}

Por último, para vincular esta clase backend al tipo de elemento en nuestra configuración, tenemos que dar a este servicio una etiqueta. Haremos esto de la misma forma que hicimos antes para el tipo de consulta: con AutoconfigureTag. De hecho, robaré este AutoconfigureTagya que estoy aquí... pegaré eso... y añadiré la declaración use para ello. Esta vez, el nombre de la etiqueta es netgen_content_browser.backend, y en lugar de type, utilizaitem_type. Ajústalo a la clave que tenemos en la config: doctrine_recipe. Pega y... ¡genial!

// ... lines 1 - 7
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('netgen_content_browser.backend', [ 'item_type' => 'doctrine_recipe' ])]
class RecipeBrowserBackend implements BackendInterface
{
// ... lines 13 - 56
}

Esta vez cuando actualizamos... el error ha desaparecido. Añadamos temporalmente una nueva Rejilla al diseño... y elijamos "Colección manual". Ahora... ¡compruébalo! Como tenemos un backend, ¡vemos un botón "Añadir elementos"! Y cuando hacemos clic en él... falla. Eso no debería sorprendernos demasiado... ya que nuestra clase backend sigue estando completamente vacía. Si quieres ver el error exacto, puedes abrir la llamada AJAX.

Creación de la clase de ubicación

El sistema del navegador de contenidos funciona así: en estos métodos, describimos una "estructura de árbol", algo así como un sistema de archivos. las "ubicaciones" son como directorios y los "elementos" son como los "archivos" o, en nuestro caso, las recetas individuales.

Vamos a simplificar mucho las cosas. En lugar de tener diferentes "directorios" o "categorías" de recetas por las que puedas navegar, vamos a tener un único directorio -o "ubicación"- en el que vivirán todas las recetas. Verás qué aspecto tiene esto en la interfaz de usuario dentro de unos minutos.

Para que esto funcione, dentro de src/ContentBrowser/, tenemos que crear una clase que represente una ubicación. La llamaré BrowserRootLocation. Esta clase... no es superinteresante: es sólo un poco de fontanería de bajo nivel que debemos tener. Haz que implemente LocationInterface, y a continuación, genera los tres métodos que necesitamos:

24 lines | src/ContentBrowser/BrowserRootLocation.php
// ... lines 1 - 2
namespace App\ContentBrowser;
use Netgen\ContentBrowser\Item\LocationInterface;
class BrowserRootLocation implements LocationInterface
{
public function getLocationId()
{
// TODO: Implement getLocationId() method.
}
public function getName(): string
{
// TODO: Implement getName() method.
}
public function getParentId()
{
// TODO: Implement getParentId() method.
}
}

De nuevo, esta clase representará la única "ubicación". Así que paragetLocationId(), podemos devolver cualquier cosa. Voy a return 0. Verás cómo se utiliza en un segundo. Para getName(), esto es lo que se mostrará en la sección de administración. Voy a return 'All'. Y para getParentId(), return null:

24 lines | src/ContentBrowser/BrowserRootLocation.php
// ... lines 1 - 6
class BrowserRootLocation implements LocationInterface
{
public function getLocationId()
{
return 0;
}
public function getName(): string
{
return 'All';
}
public function getParentId()
{
return null;
}
}

Si tienes un sistema más complejo con múltiples subdirectorios, podrías crear una jerarquía de ubicaciones.

Muy bien, actualicemos nuestra clase backend para utilizar esto. Aquí arriba, getSections()será llamado en cuanto el usuario abra el navegador de contenidos. Nuestro trabajo consiste en devolver todos los "directorios" raíz, o "ubicaciones". Nosotros sólo tenemos uno:return [new BrowserRootLocation()]:

// ... lines 1 - 10
class RecipeBrowserBackend implements BackendInterface
{
public function getSections(): iterable
{
return [new BrowserRootLocation()];
}
// ... lines 17 - 60
}

Después de llamar a éste, el navegador de contenidos llamará a getLocationId() en cada uno de ellos y hará una petición AJAX para obtener más información sobre ellos. En nuestro caso, esto ocurrirá una sola vez cuando el ID sea 0. Parece raro, pero todo lo que tenemos que hacer es devolver esa misma ubicación: if ($id === '0'), y luegoreturn new BrowserRootLocation():

// ... lines 1 - 10
class RecipeBrowserBackend implements BackendInterface
{
// ... lines 13 - 17
public function loadLocation($id): LocationInterface
{
if ($id === '0') {
return new BrowserRootLocation();
}
// ... lines 23 - 24
}
// ... lines 26 - 60
}

Fíjate en que estoy utilizando '0' como cadena, pero... en getLocationId() devolvimos un número entero:

24 lines | src/ContentBrowser/BrowserRootLocation.php
// ... lines 1 - 6
class BrowserRootLocation implements LocationInterface
{
public function getLocationId()
{
return 0;
}
// ... lines 13 - 22
}

Eso es porque el id se pasará a JavaScript y se utilizará en una llamada Ajax. Para cuando llegue aquí, será una cadena. Un pequeño detalle a tener en cuenta.

Al final, por si acaso throw a new \InvalidArgumentException() y pasa un mensaje sobre una ubicación no válida:

// ... lines 1 - 10
class RecipeBrowserBackend implements BackendInterface
{
// ... lines 13 - 17
public function loadLocation($id): LocationInterface
{
if ($id === '0') {
return new BrowserRootLocation();
}
throw new \InvalidArgumentException(sprintf('Invalid location "%s"', $id));
}
// ... lines 26 - 60
}

¡Vale! Así que nuestro backend tiene una ubicación. Para los demás métodos, devolvamos lo más sencillo posible. Deja loadItem() vacío por un momento, paragetSubLocations(), return [], para getSubLocationsCount(), return 0, paragetSubItems(), return [], para getSubItemsCount(), return 0, para search(),return []... y finalmente, para searchCount(), return 0:

// ... lines 1 - 10
class RecipeBrowserBackend implements BackendInterface
{
// ... lines 13 - 26
public function loadItem($value): ItemInterface
{
// TODO: Implement loadItem() method.
}
public function getSubLocations(LocationInterface $location): iterable
{
return [];
}
public function getSubLocationsCount(LocationInterface $location): int
{
return 0;
}
public function getSubItems(LocationInterface $location, int $offset = 0, int $limit = 25): iterable
{
return [];
}
public function getSubItemsCount(LocationInterface $location): int
{
return 0;
}
public function search(string $searchText, int $offset = 0, int $limit = 25): iterable
{
return [];
}
public function searchCount(string $searchText): int
{
return 0;
}
}

Uf... Hablaremos de cada uno de esos métodos más adelante. Pero nuestra clase backend ya es, al menos, algo funcional.

Si volvemos a actualizar el área de administración... hacemos clic en nuestra cuadrícula, y vamos a "Añadir elementos"... ¡se carga! ¡Di "hola" al navegador de contenido! Actualmente está vacío, pero puedes ver el "Todo", que es de nuestra única ubicación. Todavía no hay elementos dentro... porque tenemos que devolverlos desde getSubItems(). Hagámoslo a continuación