Re: Breadcrumb $options lost when $title is an array

I raised an issue on Github thinking it was a bug, but apparently it’s a feature. Now I have a follow-up question (for learning purposes) which I decided to ask here so that I don’t clutter Github.

The original issue

I work on a custom BreadcrumbsHelper intended to be a drop-in replacement of the Core counterpart.

class BreadcrumbsHelper extends \Cake\View\Helper\BreadcrumbsHelper
{

    /**
     * @inheritDoc
     */
    protected array $_defaultConfig = [
        'ariaCurrent' => 'last',
        'templates' => [
            'wrapper' => '<nav class="breadcrumbs container" aria-label="breadcrumb"><ol{{attrs}}>{{content}}</ol></nav>',
            'item' => '<li{{attrs}}><a href="{{url}}"{{innerAttrs}}>{{title}}</a></li>',
            'itemWithoutLink' => '<li{{attrs}}>{{title}}</li>',
            'separator' => '',
        ],
    ];

    /**
     * @inheritDoc
     */
    public function add(array|string $title, array|string|null $url = null, array $options = [])
    {
        $options = $this->addClass($options, 'breadcrumbs__list-item');
        $innerAttrs = $options['innerAttrs'] ?? [];
        $innerAttrs = $this->addClass($innerAttrs, 'breadcrumbs__link');
        $options['innerAttrs'] = $innerAttrs;
        return parent::add($title, $url, $options);
    }

    /**
     * @inheritDoc
     */
    public function render(array $attributes = [], array $separator = []): string
    {
        $attributes = $this->addClass($attributes, 'breadcrumbs__list');
        return parent::render($attributes, $separator);
    }
}

To test it, I use this (copied from some Book example):

$this->Breadcrumbs->add([
    ['title' => 'Products', 'url' => ['controller' => 'products', 'action' => 'index']],
    ['title' => 'Product name', 'url' => ['controller' => 'products', 'action' => 'view', 1234]],
]);

I expect the elements it renders to have the required CSS classes I added in my wrapper, but that doesn’t happen:

<nav class="breadcrumbs container" aria-label="breadcrumb">
    <ol class="breadcrumbs__list">
        <li><a href="/products">Products</a></li>
        <li><a href="/products/view/1234">Product name</a></li>
    </ol>
</nav>

This seems to be because the line 83 condition is true, so $options are defined empty on line 85:

Some ways around this in my add() wrapper could be:

  • detect if $title is an array; copy and inject $options into each crumb
  • detect if $title is an array; loop through each crumb; call parent::add() on the crumb individually with the options I need

None of this seems like an efficient approach. Is there any reason not to reuse the existing $options from the public function add declaration?

In other words,

 public function add(array|string $title, array|string|null $url = null, array $options = []) // <---
 { 
     if (is_array($title)) { 
         foreach ($title as $crumb) { 
             $this->crumbs[] = $crumb + ['title' => '', 'url' => null, 'options' => $options]; // <---
         } 

         return $this; 
     } 
     // ...

The answer

If $title is an array then options should be passed within each item. Update your overriding method accordingly.

github.com/cakephp/cakephp/issues/19127#issuecomment-3656342400

My followup question

I understand that sometimes a breadcrumb needs to have its own options.

But if options are the same for all breadcrumbs, isn’t it more DRY to allow passing $options once and having them apply to all breadcrumbs? Especially if $options default to [] in the function declaration anyway - so if developer does nothing, each breadcrumb ends up with [] anyway.

Do breadcrumbs need individual options predominantly often, to the point it doesn’t make sense to support shared $options?

Thank you for reading?

I agree with you :slight_smile: PR open.

Ideally, there would never be overloading on the method itself, so there would be two distinct methods add() and addMany() or alike, then this would also be cleaner.
Maybe something to be done for 6.x

2 Likes

Thank you for looking into this!