Loïc Faugeron Technical Blog

PHP array manipulation 15/10/2014

TL;DR: Redaktilo provides a nice API to manipulate array of lines (insert, replace, go to line, etc).

Arrays are surely one of the most used PHP functionality: they're simple, convenient and benefit from a large set of standard functions to manipulate them.

Let's see the different possibilities:

Finding an element

In order to find the index of a string in an array, we can use one of the following functions:

<?php

$montyPython = array(
    'This is a dead parrot',
    'No, no, it\'s not dead, it\'s resting!',
);

$exactSentence = 'This is a dead parrot';
array_search($exactSentence, $montyPython, true); // Returns 0
$regex = '/resting!$/';
$found = preg_grep($regex, $montyPython);
key($found); // Returns 1

Note: The third parameter of array_search enables the strict comparison (it makes sure the types are also the same).

We can find the matching elements of a pattern using preg_grep. To get the index of the first result found, we can use key.

Finding an element relatively to a given index

It's great! Now what if we want to remember the index and then make a search relatively to it? For example we want to find the index of the element /a knewt/, but only in the elements above ... I got better...:

<?php

$holyGrail = array(
    'She turned me into a knewt!',
    'A knewt?',
    '... I got better...',
    'A WITCH!',
);

$index = array_search('... I got better...', $holyGrail, true);
$section = array_slice($holyGrail, 0, $index, true);
$reversedSection = array_reverse($section, true);
$found = preg_grep('/a knewt/', $reversedSection);
key($found); // Returns 0

In this code snippet, we get a subset of the array (from the first element to ... I got better) using array_slice (the last argument allows to keep the indexes unchanged). This also excludes ... I got better....

Then we change the order of the elements using array_reverse (the second argument allows to keep the index unchanged) so the element above ... I got better... would be the first result.

Finally we look for /a knewt/ as we did previously.

Inserting a new element

If we want to insert a new element after a given index in the array, we need to use array_splice:

<?php

$grandPiano = array(
    'I would like to learn how to fly an aeroplane',
    'A what?',
    'An aeroplane',
    'OH! An aeroplane, oh I say we are grand aren\'t we?',
    'Pardon me while I\'m flying me aeroplane... NOW GET ON THE TABLE!',
);

$found = preg_grep('/grand/', $grandPiano);
$index = key($found);
array_splice($grandPiano, $index + 1, 0, 'No more cluttered cream for me, butler, I\'m off to play the grand piano');

Actually array_splice allows to replace a subsection of an array, here's the explanation of its arguments:

  1. the array to manipulate
  2. the starting index
  3. the ending index (0 means replacing nothing, therefore just inserting)
  4. the replacement (in our case the element to insert)

It's not very explicit, but we have a solution for this (keep reading to find out).

Removing an element

Here's an easy one:

<?php

$parrot = array(
    'Hello, I would like to make a complain. Hello, miss?',
    'What do you mean miss?',
    'Sorry, I have a cold',
);

$index = array_search('Sorry, I have a cold', $parrot, true);
unset($parrot[$index]);

You might want to reorder the indexes, to avoid gaps:

<?php

$parrot = array(
    'Hello, I would like to make a complain. Hello, miss?',
    'What do you mean miss?',
    'Sorry, I have a cold',
);

$index = array_search('What do you mean miss?', $parrot, true);
unset($parrot[$index]);
// Current value:
// $parrot = array(
//     0 => 'Hello, I would like to make a complain. Hello, miss?',
//     2 => 'Sorry, I have a cold',
// );

$parrot = array_values($parrot);
// Current value:
// $parrot = array(
//     0 => 'Hello, I would like to make a complain. Hello, miss?',
//     1 => 'Sorry, I have a cold',
// );

The array_values function is equivalent to:

$newArray = array();
foreach ($oldArray as $element) {
    $newArray[] = $element;
}

Retrieving elements from a file

Until now, we've been using a hard coded array, but this is rarely what we encounter in real life project. The data could for example come from a file, which can be transformed into an array of lines:

<?php

$file = file('/tmp/holy-grail.text', FILE_IGNORE_NEW_LINES);

Note: the second argument will remove the trailing line breaks.

The only problem with file is that it will remove the last line if it's empty. Let's use another function:

<?php

$content = file_get_contents('/tmp/holy-grail.txt');
$lines = explode("\n", $content);

The file_get_contents function returns the content as a string.

We used explode to split it into an array of lines.

This assumes that the file hasn't been created on Windows (where the line separator is \r\n)... We need to detect the line break:

<?php

$content = @file_get_contents('/tmp/holy-grail.txt');
$lineBreak = "\n"; // Used by every systems (except Windows), so used as default
if (false === strpos($content, "\n")) { // No line break detected at all
    $lineBreak = PHP_EOL; // Using the system's one
}
if (false !== strpos($content, "\r\n")) { // Windows line break detected
    $lineBreak = "\r\n";
}
$lines = explode($lineBreak, $content);

Note: There's many check to be done before actually reading the file (does the file actually exists? Do we have the permission to read it?). We have a solution for this (keep reading to find out).

Saving changes in a file

If we do any changes to those lines, we might want to save them on the filesystem:

<?php

$lines = array(
    'Morning, morning, morning',
    'Morning Jim, Morning Jack',
    'Can\'t complain, keep coming back',
    'Boring, boring, boring',
);
$lineBreak = "\n"; // Or whatever has been detected
$content = implode($lineBreak, $lines);
file_put_contents('/tmp/silly-walk-song.txt', $content);

To convert back the array of lines to a string content, we use implode.

To write the content in the file, we use file_put_contents.

Note: There's many check to be done before actually writing in a file (does the path actually exists? Do we have the permissions? What happens if the writing process fails during the execution?). To solve this, use the Symfony2 Filesystem Component (Filesystem#dumpFile()).

There is a better way

You don't find array_splice very explicit for element insertion (or can't find a way to remember its argument order/meaning)?

Keeping the elements, the index, the line break in separates variables looks too procedural for you?

And what about error management?

Don't panic! There is a better way: Redaktilo (it means "Editor" in esperanto).

This small library makes array manipulation easier by providing:

Use it in your projects, thanks to Composer:

composer require 'gnugat/redaktilo:~1.1'
<?php

require __DIR__.'/vendor/autoload.php';

use Gnugat\Redaktilo\EditorFactory;

$editor = EditorFactory::createEditor();

Note: In order to make operations more explicit, Redaktilo has adopted the vocabulary of file edition (more specifically manipulation of lines). But in the end it's still array manipulation.

Retrieving a line

You don't have to worry about file checking and line break detection anymore:

<?php

require __DIR__.'/vendor/autoload.php';

use Gnugat\Redaktilo\EditorFactory;

$editor = EditorFactory::createEditor();
$file = $editor->open('/tmp/silly-walk-song.txt');
$file->getLineBreak(); // Returns "\n" if the file hasn't been created on Windows

Finding an element relatively to a given line number

Redaktilo takes care of the search strategy for you (it uses preg_grep when you give a valid regular expression, and array_search when you give a string).

It supports search relative to the current line number stored in the given Text and File (it uses array_slice, array_reverse and key internally).

The hasAbove and hasBelow methods just return a boolean, while the jumpAbove and jumpBelow methods rather store the found line number in the given Text and File (and raise an exception if nothing is found):

<?php

require __DIR__.'/vendor/autoload.php';

use Gnugat\Redaktilo\EditorFactory;

$editor = EditorFactory::createEditor();
$file = $editor->open('/tmp/silly-walk-song.txt');

$editor->jumpBelow($file, 'Boring, boring, boring');
$file->getCurrentLineNumber(); // Returns 3

$editor->hasAbove($file, '/morning,/'); // Returns true

Note: hasAbove, hasBelow, jumpAbove and jumpBelow all have a third argument which is a line number. If provided, the search will be done relatively to this line number, rather than to the current one.

For example, checking the presence of a pattern in the whole file can be done as: $editor->hasBelow($file, $pattern, 0); // starts the search from the top of the file.

Inserting a new line

No more array_splice nonsense!

<?php

require __DIR__.'/vendor/autoload.php';

use Gnugat\Redaktilo\EditorFactory;

$editor = EditorFactory::createEditor();
$file = $editor->open('/tmp/silly-walk-song.txt');

$editor->insertAbove($file, 'The silly walk song');

Note: insertAbove, insertBelow, replace and remove all have a third argument which is a line number. If provided, the anipulation will be done relatively to it, instead of relatively to the current one.

Using Text

If you're not manipulating a file, you can use Text just like we used File:

<?php

require __DIR__.'/vendor/autoload.php';

use Gnugat\Redaktilo\EditorFactory;
use Gnugat\Redaktilo\Service\LineBreak;
use Gnugat\Redaktilo\Service\TextFactory;

$lineBreak = new LineBreak();
$textFactory = new TextFactory($lineBreak);

$text = $textFactory->make(<<<EOF
Some raw text you would have got from somewhere,
for example a database.
EOF
);

$editor = EditorFactory::createEditor();
$editor->hasBelow($text, '/a database/'); // Returns true

Cheat Sheet

There's many more operations available, as you can discover in the documentation.

To make it easier, here's some cheat sheet.

Editor

<?php

namespace Gnugat\Redaktilo;

use Gnugat\Redaktilo\Search\PatternNotFoundException;
use Gnugat\Redaktilo\Search\SearchEngine;
use Gnugat\Redaktilo\Service\Filesystem;
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
use Symfony\Component\Filesystem\Exception\IOException;

class Editor
{
    public function open($filename, $force = false); // @throws FileNotFoundException
    public function save(File $file); // @throws IOException If cannot write

    // @throw PatternNotFoundException
    public function jumpAbove(Text $text, $pattern, $location = null);
    public function jumpBelow(Text $text, $pattern, $location = null);

    // @return bool
    public function hasAbove(Text $text, $pattern, $location = null);
    public function hasBelow(Text $text, $pattern, $location = null);

    public function insertAbove(Text $text, $addition, $location = null);
    public function insertBelow(Text $text, $addition, $location = null);
    public function replace(Text $text, $replacement, $location = null);
    public function remove(Text $text, $location = null);
}

Text

<?php

namespace Gnugat\Redaktilo;

class Text
{
    public function getLines();
    public function setLines(array $lines);
    public function getLength();
    public function getLineBreak();
    public function setLineBreak($lineBreak);
    public function getCurrentLineNumber();

    // @throw InvalidLineNumberException
    public function setCurrentLineNumber($lineNumber);
    public function getLine($lineNumber = null);
    public function setLine($line, $lineNumber = null);
}

File

<?php

namespace Gnugat\Redaktilo;

class File extends Text
{
    public function getFilename();
    public function setFilename($filename);
}

Conclusion

PHP provides plenty of built-in functions to manipulate arrays, but those are not enough. Redaktilo is a small library which provides a nicer API.

It allows you to select a line relatively to the current one and then do CRUD operations on it.

I hope you'll find it as usefull as I do and if you find any bug or have any proposals feel free to do so on Github where Loïck Piera and myself will both be glad to help you.

Note: As for the 15/10/2014, the current version of Redaktilo is 1.1.6 (stable). Future updates are already planned!