Collection's append() and prepend() behave differently when single argument provided

Does prepend() differ from append() in anything else than where it places the element ?

The following code seems to suggest that it does differ. If I use append I get this output:

<?php
	$children = $site->children()->filterBy('intendedtemplate', 'programme')->listed()->not(page('vessel')) ;
	$children->append(page('vessel')); // <-- append
	dump($children);
?>

Kirby\Cms\Pages Object
(
    [0] => linstituto-per-limaginazione-del-mediterraneo
    [1] => hofajfds
    [2] => radio-materiality
    [3] => among-many-of-us
    [4] => vessel // <-- ok
)

If I use prepend instead, the output is unexpected:

<?php
	$children = $site->children()->filterBy('intendedtemplate', 'programme')->listed()->not(page('vessel')) ;
$children->prepend(page('vessel')); // <-- prepend
	dump($children);
?>

Kirby\Cms\Pages Object
(
    [0] => 0 // <-- what ?
    [1] => linstituto-per-limaginazione-del-mediterraneo
    [2] => hofajfds
    [3] => radio-materiality
    [4] => among-many-of-us
)

Thank you

These are the two methods from the Collection class:

public function append(...$args)
{
    if (count($args) === 1) {
        if (is_object($args[0]) === true) {
            $this->data[$args[0]->id()] = $args[0];
        } else {
            $this->data[] = $args[0];
        }
    } elseif (count($args) === 2) {
        $this->set($args[0], $args[1]);
    }
    return $this;
}
public function prepend(...$args)
{
    if (count($args) === 1) {
        array_unshift($this->data, $args[0]);
    } elseif (count($args) === 2) {
        $data = $this->data;
        $this->data = [];
        $this->set($args[0], $args[1]);
        $this->data += $data;
    }
    return $this;
}

As you can see, they do work differently.

The add() method, however, is not a method of the Collection class, but of the Pages class that extends the Collection class.

On a side note, this line

$children = new Pages();

In your example above is superfluous.

So if I am reading the code well, if only 1 argument is provided append() uses the page ID as key when adding the page to the data array.

But prepend does not. Instead, it uses php’s unshift() to add the element to the beggining of the data array.

As a result, in my output, when using prepend without a key I get ‘0’ which would be the key of the keyless page in the first position of the data array.

If that is correct, then… why the difference ? sorry if there is an obvious reason, but it does kind of feel counterintuitive.

True, I mixed the classes there.

I see that, but in this previously linked unsolved Kirby2 question, using new Pages() was a way to fix an error. That added to my confusion, and made me use new Pages() everywhere.

Thank you

Of course you can use an empty Pages object as a starting point and then add to that. But in your examples above, you are not adding, prepending etc. to your empty Pages collection, but you first redefine your $children variable, then add to it, which just doesn’t make sense.

Ah yes, that is true, and it is nonsense, I edited it out of my question. Thanks for pointing it out.

Could you please answer my question regarding the difference between prepend and append if possible?

Danke

I’d love to, but I currently I don’t have an answer for you. I’ll try to think about it when my brain works better.

hah! ok thank you very much.

I would open an issue, or at least suggest adding this difference as info on the reference page, but I am afraid to be missing an obvious answer.

Next time I encounter this I may do it.

Thank you

I actually went and compared Kirby2 and Kirby3 reference and code for append() and prepend()

In K2 it seems both append and prepend required both key and page to be provided. And prepend used union operator to add the key->object, as an array, to itself

  /**
   * Appends an element to the data array
   *
   * @param string $key
   * @param mixed $object
   * @return Collection
   */
  public function append($key, $object) {
    $this->data = $this->data + array($key => $object);
    return $this;
  }

  /**
   * Prepends an element to the data array
   *
   * @param string $key
   * @param mixed $object
   * @return Collection
   */
  public function prepend($key, $object) {
    $this->data = array($key => $object) + $this->data;
    return $this;
  }

… while pages->add() was the only method that did not require a key to be specified, probably because you can pass either a page or a collection of pages as argument, so if not present the method obtains the key, just like k3 collection->append() (but NOT k3 collection->prepend()):

  /**
   * Adds a single page object to the
   * collection by id or the entire object
   *
   * @param mixed $page
   */
  public function add($page) {

    if(is_a($page, 'Collection')) {
      foreach($page as $object) $this->add($object);
    } else if(is_string($page) and $object = page($page)) {
      $this->data[$object->id()] = $object;
    } else if(is_a($page, 'Page')) {
      $this->data[$page->id()] = $page;
    }

    return $this;

  }

In the reference, the need to provide two arguments to append and prepend was much clear in K2 imo (see links).

In k3 append() the key is ‘optional’ in the sense that if it is not provided, and the element appended is an object the method will find the key for you.

public function append(...$args)
{
    if (count($args) === 1) {
        if (is_object($args[0]) === true) {
            $this->data[$args[0]->id()] = $args[0];
        } else {
            $this->data[] = $args[0];
        }
    } elseif (count($args) === 2) {
        $this->set($args[0], $args[1]);
    }
    return $this;
}

While in K3 prepend(), as said, not providing a key results in the element being added without key, as the method uses array_unshift().

public function prepend(...$args)
{
    if (count($args) === 1) {
        array_unshift($this->data, $args[0]);
    } elseif (count($args) === 2) {
        $data = $this->data;
        $this->data = [];
        $this->set($args[0], $args[1]);
        $this->data += $data;
    }
    return $this;
}

This could be considered ‘optional’ too, I guess, but I do not think it provides the expected results, and the lack of key will probably make some possibly chained methods fail or provide an unexpected result. perhaps?

Plus this difference becomes obvious only by checking the code.

I am quite a newbie in php, but wouldn’t it be better that prepend does exactly as append, checking if element is an object, then finding its key, and calling itself with two arguments? reverting to unshift only when the element is NOT and object?

public function prepend(...$args)
{
    if (count($args) === 1) {
        if (is_object($args[0]) === true) {
            $this->prepend($args[0]->id(), $args[0]);
        } else {
            array_unshift($this->data, $args[0]);
        }
    } elseif (count($args) === 2) {
        $data = $this->data;
        $this->data = [];
        $this->set($args[0], $args[1]);
        $this->data += $data;
    }
    return $this;
}

I’ve opened an issue.

And closed :slight_smile: https://github.com/getkirby/kirby/issues/2078#issuecomment-531682005