Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Navegador de Contenidos: Devolver los elementos

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Nuestro Navegador de Contenidos está funcionando más o menos. Podemos ver nuestra única ubicación... sólo que aún no tenemos ningún resultado. Esto se debe a que, para cualquier ubicación seleccionada, el Navegador de Contenidos llama a getSubItems(). Nuestro trabajo aquí es devolver los resultados. En este caso, todas nuestras recetas. Si tuviéramos varias ubicaciones, como recetas divididas en categorías, podríamos utilizar la variable $location para devolver el subconjunto. Pero haremos la consulta y devolveremos todas las recetas.

Consulta en getSubItems()

Para ello, ve a la parte superior de la clase y crea un constructor conprivate RecipeRepository $recipeRepository:

... lines 1 - 4
use App\Repository\RecipeRepository;
... lines 6 - 11
class RecipeBrowserBackend implements BackendInterface
{
public function __construct(private RecipeRepository $recipeRepository)
{
}
... lines 17 - 70
}

Luego, aquí abajo en getSubItems(), di $recipes = $this->recipeRepository y utiliza el mismo método de antes: ->createQueryBuilderOrderedByNewest(). A continuación añade ->setFirstResult($offset)... y ->setMaxResults($limit). El Navegador de Contenidos viene con la paginación incorporada. Nos pasa el desplazamiento y el límite de la página en la que se encuentre el usuario, lo introducimos en la consulta y todos contentos. Termina con getQuery() y getResult():

... lines 1 - 11
class RecipeBrowserBackend implements BackendInterface
{
... lines 14 - 46
public function getSubItems(LocationInterface $location, int $offset = 0, int $limit = 25): iterable
{
$recipes = $this->recipeRepository
->createQueryBuilderOrderedByNewest()
->setFirstResult($offset)
->setMaxResults($limit)
->getQuery()
->getResult();
}
... lines 56 - 70
}

Fíjate en que getSubItems() devuelve un iterable... en realidad se supone que es un iterable de algo llamado ItemInterface. Así que no podemos devolver simplemente estos objetos Recipe.

Crear la clase envolvente ItemInterface

En su lugar, en src/ContentBrowser/, crea otra clase llamada, qué talRecipeBrowserItem. Haz que implemente ItemInterface -la deNetgen\ContentBrowser - y genera los cuatro métodos que necesita:

... lines 1 - 2
namespace App\ContentBrowser;
use Netgen\ContentBrowser\Item\ItemInterface;
class RecipeBrowserItem implements ItemInterface
{
public function getValue()
{
// TODO: Implement getValue() method.
}
public function getName(): string
{
// TODO: Implement getName() method.
}
public function isVisible(): bool
{
// TODO: Implement isVisible() method.
}
public function isSelectable(): bool
{
// TODO: Implement isSelectable() method.
}
}

Esta clase será una pequeña envoltura de un objeto Recipe. Observa: añade un método __construct() con private Recipe $recipe:

... lines 1 - 4
use App\Entity\Recipe;
... lines 6 - 7
class RecipeBrowserItem implements ItemInterface
{
public function __construct(private Recipe $recipe)
{
}
... lines 13 - 32
}

Ahora, para getValue(), esto debería devolver el "identificador", así quereturn $this->recipe->getId(). Para getName(), sólo necesitamos algo visual que podamos mostrar, como$this->recipe->getName(). Y para isVisible(), return true. Esto es útil si un Recipe puede estar publicado o no. Tenemos una situación similar conisSelectable():

... lines 1 - 7
class RecipeBrowserItem implements ItemInterface
{
... lines 10 - 13
public function getValue()
{
return $this->recipe->getId();
}
public function getName(): string
{
return $this->recipe->getName();
}
public function isVisible(): bool
{
return true;
}
public function isSelectable(): bool
{
return true;
}
}

Si tuvieras un conjunto de reglas en las que quisieras mostrar ciertas recetas pero hacer que no se pudieran seleccionar, podrías return false aquí.

Y... ¡ya está! ¡Ha sido fácil!

De vuelta a nuestra clase backend, necesitamos convertir estos objetos Recipe en objetosRecipeBrowserItem. Podemos hacerlo con array_map(). Volveré a utilizar la elegante sintaxis fn(), que recibirá un argumento Recipe $recipe, seguido de => new RecipeBrowserItem($recipe). Para el segundo arg, pasa $recipes:

... lines 1 - 12
class RecipeBrowserBackend implements BackendInterface
{
... lines 15 - 47
public function getSubItems(LocationInterface $location, int $offset = 0, int $limit = 25): iterable
{
... lines 50 - 55
return array_map(fn(Recipe $recipe) => new RecipeBrowserItem($recipe), $recipes);
}
... lines 59 - 73
}

Es una forma elegante de decir

Recorre todas las recetas del sistema, crea un nuevo RecipeBrowserItem para cada una, y devuelve esa nueva matriz de elementos.

Muy bien, ¡vamos a ver qué aspecto tiene! Actualiza el diseño, haz clic en la Rejilla, vuelve a "Añadir elementos" y... ¡ya está! ¡Vemos diez elementos!

Implementando getSubItemsCount()

Pero deberíamos tener varias páginas. Ah, eso es porque seguimos devolviendo 0desde getSubItemsCount(). Vamos a arreglarlo. Roba la consulta de arriba... pega, devuelve esto, quita setFirstResult() y setMaxResults(), añade ->select('COUNT(recipe.id)'), y luego llama a getSingleScalarResult()al final:

... lines 1 - 12
class RecipeBrowserBackend implements BackendInterface
{
... lines 15 - 59
public function getSubItemsCount(LocationInterface $location): int
{
return $this->recipeRepository
->createQueryBuilderOrderedByNewest()
->select('COUNT(recipe.id)')
->getQuery()
->getSingleScalarResult();
}
... lines 68 - 77
}

Y así, cuando actualicemos... y abramos el Navegador de Contenidos... ¡tendremos páginas!

Añadir la función de búsqueda

Vale, pero ¿podemos buscar recetas? Por supuesto. Podemos aprovechar search() ysearchCount(). Esto es muy sencillo. Roba toda la lógica de getSubItems(), pégala en search() y pasa $searchText al método QueryBuilder, que ya permite este argumento:

... lines 1 - 12
class RecipeBrowserBackend implements BackendInterface
{
... lines 15 - 68
public function search(string $searchText, int $offset = 0, int $limit = 25): iterable
{
$recipes = $this->recipeRepository
->createQueryBuilderOrderedByNewest($searchText)
->setFirstResult($offset)
->setMaxResults($limit)
->getQuery()
->getResult();
return array_map(fn(Recipe $recipe) => new RecipeBrowserItem($recipe), $recipes);
}
... lines 80 - 88
}

Si quieres tener un poco menos de duplicación de código, podrías aislar esto en un método private en la parte inferior.

Copia también la lógica del otro método de recuento... pégalo en searchCount(), y pásalo también a $searchText:

... lines 1 - 12
class RecipeBrowserBackend implements BackendInterface
{
... lines 15 - 80
public function searchCount(string $searchText): int
{
return $this->recipeRepository
->createQueryBuilderOrderedByNewest($searchText)
->select('COUNT(recipe.id)')
->getQuery()
->getSingleScalarResult();
}
}

Y así de fácil, si nos movemos hacia aquí e intentamos buscar... funciona. ¡Estupendo!

Muy bien - selecciona algunos elementos, pulsa "Confirmar" y... ¡oh no! ¡Se rompe! Sigue diciendo "Cargando". Si miras hacia abajo en la barra de herramientas de depuración web, tenemos un error 400. Maldita sea. Cuando lo abrimos, vemos

El cargador de valores para el tipo de valor doctrine_recipe no existe.

Sólo nos falta una pieza final: Una clase muy sencilla llamada "cargador de valores". Eso a continuación.

Leave a comment!

5
Login or Register to join the conversation
Joris-Mak Avatar
Joris-Mak Avatar Joris-Mak | posted hace 7 meses

I see on my setup, 'searchItems' and 'searchItemsCount' are called. Then there is an error because they don't return anything, so it returns null, which isn't an int, and something blows up :wink:.

I also see search and searchCount are marked deprecated?

I had to move the code to searchItems / searchItemsCount. The parameter you receive there is a SearchQuery object that contains basically the same as before (searchText, offset, limit).

But to return results, you have to return an object, that will return the results...
So instead of doing the array_map, you have to create a little wrapper-object that implements SearchResultInterface, that contains a single method that returns the items.

I just made an object that receives the $recipes array in the constructor, and in the getResults() function does the array_map() call from your tutorial. Just another piece of boiletplate, but I guess it's needed for more advanced stuff or they wouldn't be doing it.

Reply

Hey Joris-Mak!

I see on my setup, 'searchItems' and 'searchItemsCount' are called. Then there is an error because they don't return anything, so it returns null, which isn't an int, and something blows up :wink:.

We do fill in these methods later. But, even without using the search functionality, when the methods are blank, you get an error? When does the error happen - right when the content browser loads?

I also see search and searchCount are marked deprecated?

Ah, darn! I had missed that!

I had to move the code to searchItems / searchItemsCount. The parameter you receive there is a SearchQuery object that contains basically the same as before (searchText, offset, limit).

I'll have to do something similar and add a note for it. If you have the code handy, I'd love it if you shared ;). But no big deal either way. Thanks for pointing this out.

Cheers!

Reply
Joris-Mak Avatar

We do fill in these methods later. But, even without using the search functionality, when the methods are blank, you get an error? When does the error happen - right when the content browser loads?

Oh, maybe I have to be more clear: The error is about searching. So I get an ajax-call error when trying to search in the content-browser.

And again, the error seems to be my editor already adding searchItems and searchItemsCount as well as search and searchCount, when I ask it to 'implement all abstract methods'.

So I get the new methods, and the old methods. Then the new ones are called, and I didn't fill those in because I was following your tutorial to the letter :P.

Reply
Joris-Mak Avatar
Joris-Mak Avatar Joris-Mak | weaverryan | posted hace 7 meses | edited
public function searchItems(SearchQuery $searchQuery)
    {
        $recipes = $this->recipeRepository
            ->createQueryBuilderOrderedByNewest($searchQuery->getSearchText())
            ->setFirstResult($searchQuery->getOffset())
            ->setMaxResults($searchQuery->getLimit())
            ->getQuery()
            ->getResult();
        return new RecipeBrowserSearchResults($recipes);
    }
    
    public function searchItemsCount(SearchQuery $searchQuery)
    {
        return $this->recipeRepository
            ->createQueryBuilderOrderedByNewest($searchQuery->getSearchText())
            ->select('COUNT(recipe.id)')
            ->getQuery()
            ->getSingleScalarResult();
    }

With my search-result-class (in RecipeBrowserSearchResults.php):

<?php

declare(strict_types=1);

namespace App\ContentBrowser;

use Netgen\ContentBrowser\Backend\SearchResultInterface;
use App\Entity\Recipe;

class RecipeBrowserSearchResults implements SearchResultInterface
{
    /**
     * @param Recipe[]
     */
    public function __construct(private array $results)
    {
    }

    public function getResults(): iterable
    {
        return array_map(fn (Recipe $recipe) => new RecipeBrowserItem($recipe), $this->results);
    }
}

The error I'm getting if I don't do it this way:

Netgen\ContentBrowser\Pager\ItemSearchAdapter::getNbResults(): Return value must be of type int, null returned

But a little lightbulb goes on in my head:
Actually, not implementing the searchItems() and searchItemsCount() functions fixes this error as well!

If I dive into the BackendInterface that we're implementing, I'm seeing searchItems and searchItemsCount commented out in the interface, with a note that they will be added in 2.0.

So... I guess this is my editor making a mistake, and seeing searchItems() and searchItemsCount() as already part of the interface. Removing them even marks an error that they need to be implemented, but I think VSCode-Intelephense is misreading the commented out line in the interface.

They do work (magic methods I'm guessing). and search and searchCount are marked as deprecated and to-be-removed from 2.0. But I haven't read their docs yet - just this tutorial.

So I guess this is no issue yet, and I was a little bit quick on the draw. But you at least have a heads up that this change is incoming :) (or new code can already be made more future-proof by using the new methods).

Reply

Hey Joris-Mak!

Ah, this is great! I will try it out, then get a note added. Because, yes, while what I have works, we'd like to have you code the most future-proof / new version.

So... I guess this is my editor making a mistake, and seeing searchItems() and searchItemsCount() as already part of the interface. Removing them even marks an error that they need to be implemented, but I think VSCode-Intelephense is misreading the commented out line in the interface.

This might actually be VSCode being really smart! The methods weren't actually added to the interface right now because that would break backwards compatibility. The commented-out versions are a way to hint to users & editors that there is a new way of doing things. Usually, this means that you:

A) Should implement these new, commented-out methods fully.
B) Should add the old methods (because you have to... as they are still part of the interface) but leave them blank.

If the system is built correctly, what they usually do is look to see if the new methods exist on the class. If they do, they call those (and ignore the old methods). If they do not, it would trigger a deprecation then call the old methods. But I'll play with this code to see if this is how it behaves or not :).

Thanks again!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.13", // 2.13.3
        "easycorp/easyadmin-bundle": "^4.4", // v4.4.1
        "netgen/layouts-contentful": "^1.3", // 1.3.2
        "netgen/layouts-standard": "^1.3", // 1.3.1
        "pagerfanta/doctrine-orm-adapter": "^3.6",
        "sensio/framework-extra-bundle": "^6.2", // v6.2.8
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/console": "5.4.*", // v5.4.14
        "symfony/dotenv": "5.4.*", // v5.4.5
        "symfony/flex": "^1.17|^2", // v2.2.3
        "symfony/framework-bundle": "5.4.*", // v5.4.14
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "5.4.*", // v5.4.6
        "symfony/runtime": "5.4.*", // v5.4.11
        "symfony/security-bundle": "5.4.*", // v5.4.11
        "symfony/twig-bundle": "5.4.*", // v5.4.8
        "symfony/ux-live-component": "^2.x-dev", // 2.x-dev
        "symfony/ux-twig-component": "^2.x-dev", // 2.x-dev
        "symfony/validator": "5.4.*", // v5.4.14
        "symfony/webpack-encore-bundle": "^1.15", // v1.16.0
        "symfony/yaml": "5.4.*", // v5.4.14
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "5.4.*", // v5.4.11
        "symfony/maker-bundle": "^1.47", // v1.47.0
        "symfony/stopwatch": "5.4.*", // v5.4.13
        "symfony/web-profiler-bundle": "5.4.*", // v5.4.14
        "zenstruck/foundry": "^1.22" // v1.22.1
    }
}
userVoice