Tag Archives: PHP

PHP: Unset all defined variables

foreach(get_defined_vars() as $k => $v)
    unset($$k);
unset($k, $v);

Handy in for example this setting:

foreach($iterable as $item)
{
    extract($item);
    unset($item);

    // Define another variable, for only some of the items
    if($foo == 'bar')
        $x = 2;

    // Yield all defined vars
    yield get_defined_vars();

    // Cleanup, to prevent $x and other variables from
    // sticking around to the next iteration
    foreach(get_defined_vars() as $k => $v)
        unset($$k);
    unset($k, $v);
}

PHP: Pathable RecursiveIteratorIterator

Needed to recursively loop through a multi-dimensional array and print out each leaf-node with its full “path”.

For this I used an RecursiveArrayIterator for the array and a RecursiveIteratorIterator for the recursion. Thought I was home free because I had used a method called getSubpathname before, but turned out that was just something the RecursiveDirectoryIterator had…

So, had to grow my own… noting it here for others and the future:

class PathableRecursiveIteratorIterator
    extends RecursiveIteratorIterator
{
    /**
     * Gets the path to current node, i.e. each
     *   key "upwards", including self.
     *
     * @param null|string $glue Optional $glue for implode().
     *
     * @return array|string The keys, from root to self,
     *   as an array; or as a string if $glue is provided.
     */

    public function getPath($glue = null)
    {
        for($i = 0; $i < $this->getDepth(); $i++)
            $path[] = $this->getSubIterator($i)->key();

        $path[] = $this->key();

        return $glue !== null
            ? implode($glue, $path)
            : $path;
    }
}

Based upon: StackOverflow

Edit composer dependencies “inline” while developing

Have a PHP project, and want to re-use some classes in a new project. Moving them to their own repository and turning them into a Composer dependency is a clean way to do that. If hosted on GitHub/BitBucket, it’s even simply to be a bit more proper and fancy by publishing the package on Packagist with automatic updates based on git tags. However, if still heavily developing both the project and the dependency, the round trip through repo/packagist is a pain.

But today I discovered there’s an option called --prefer-source which seems to solve most of this pain. And here’s a basic note-to-self on how to get that to work…

0. Make sure dependency is a composer dependency

// Dependency composer.json
{
    "name": "my/package",
    "autoload":
    {
        "psr-4": {"": "src/"}
    }
}

1. Add dependency repo and package to root project

// Root project composer.json
{
    "repositories":
    [
        {"type": "vcs", "url": "https://github.com/username/my-project"}
    ],
    "require":
    {
        "my/project": "dev-master",
    }

2. Run update with –prefer-source

$ composer require my/package dev-master --prefer-source

We should now have the package downloaded and, more importantly, if you check ./vendor/my/package it should have the .git directory, meaning you can make immediately working changes there directly, and commit when you’re happy… Our other root project(s) depending on it should then get the update from the source repository after an easy composer update. 👍


Note: I’m a bit fuzzy on what composer does to keep track on whatever different happens through --prefer-source, and it’s an option for both composer install and composer update. For example, at first attempt, I tried to use composer update --prefer-source on a dependency that had already been downloaded, and the .git directory did not turn up, but if I just deleted the vendor directory for that package and then re-ran the command, then the .git was there.

So, feel free to comment if you have some light on that topic 😛🤓

PHP: Validating flexible/incomplete date time strings

Need to validate some datetime strings, that may or may not be incomplete. Might be for example just a year and a month, while the rest is unknown.

Noting it here in case I need it again. And in case someone else needs it, knows a more efficient/cleaner way, or sees a flaw…

function flexi_time($value): bool
{
    $valid = preg_match('/^(?<year>\d{4})(?:-(?<month>\d{2})(?:-(?<day>\d{2})(?:[ T](?<hour>\d{2}):(?<min>\d{2})(?::(?<sec>\d{2}))?)?)?)?$/', $value, $matches);
   
    if( ! $valid)
        return false;

    extract($matches);

    // Check month
    if($month ?? null AND ! between($month, 1, 12))
        return false;

    // Check date
    if($day ?? null AND ! checkdate($month, $day, $year))
        return false;

    // Check hour
    if($hour ?? null AND ! between($hour, 0, 23))
        return false;

    // Check minute
    if($min ?? null AND ! between($min, 0, 59))
        return false;

    // Check second
    if($sec ?? null AND ! between($sec, 0, 59))
        return false;

    return true;
}

function between($value, $min, $max): bool
{
    return $value >= $min && $value <= $max;
}

Test

$dates = [
    'foo', // Invalid
    '17', // Invalid
    '2017',
    '2017-01',
    '2017-13', // Invalid month
    '2017-01-17',
    '2017-02-31', // Invalid date
    '2017-01-17 20', // Invalid hour without minutes
    '2017-01-17 20:00',
    '2017-01-17T20:00', // Both space and T allowed as separator
    '2017-01-17 20:00:10',
    '2017-01-17 25:00:10', // Invalid hour
    '2017-01-17 20:70:70', // Invalid minute
    '2017-01-17 20:10:70', // Invalid second
];
print_r(array_filter($dates, 'flexi_time'));
Array
(
    [2] => 2017
    [3] => 2017-01
    [5] => 2017-01-17
    [8] => 2017-01-17 20:00
    [9] => 2017-01-17T20:00
    [10] => 2017-01-17 20:00:10
)

PHP: preg_match_all_callback

There are several PCRE functions available, but today I looked for one that just wasn’t there: preg_match_all_callback().

Could’ve maybe used preg_replace_callback(), but felt wrong since I didn’t actually want to do any replacing. I just needed my function to be called for each match.

So I wrote it myself. Noting it here, in case I (or someone else) needs it again.

<?php
/**
 * Perform a global regular expression match
 * and calls the callback for each match.
 */

function preg_match_all_callback(
        string $pattern,
        string $subject,
        callable $callback)
{
    $r = preg_match_all($pattern, $subject, $matches, PREG_SET_ORDER);
    foreach($matches ?? [] as $match)
        $callback($match);
    return $r;
}

And, in case someone reads this post and knows it actually does exist… and if that someone is you, please do leave a comment!

And, yes, I could’ve just written those 3 lines where I needed them, but what’s the fun in that? And besides, the shorter the code where it counts, the easier what counts is to read.

Usage

preg_match_all_callback('/(\w)\w*/', 'Hello World', 'var_dump');
array (size=2)
  0 => string 'Hello' (length=5)
  1 => string 'H' (length=1)

array (size=2)
  0 => string 'World' (length=5)
  1 => string 'W' (length=1)

PHP: Get headers with actual HEAD request

PHP has a function called get_headers which, as you’d guess, gives you the headers returned from an HTTP request. However it actually uses a GET, rather than HEAD, request.

Figured out you can change this by setting a stream context, so wrapped it in a function. And posting it here in case I need it again.

Also added a cleanup of the returned array, as I found it a bit ugly when the request included redirects. See difference below code.

Note: I silence the get_headers call because it throws several warnings, e.g. if the hostname fails lookup, and I’m not really interested in why it fails.

function get_head(string $url, array $opts = [])
{
    // Store previous default context
    $prev = stream_context_get_options(stream_context_get_default());

    // Set new one with head and a small timeout
    stream_context_set_default(['http' => $opts +
        [
            'method' => 'HEAD',
            'timeout' => 2,
        ]]);

    // Do the head request
    $req = @get_headers($url, true);
    if( ! $req)
        return false;

    // Make more sane response
    foreach($req as $h => $v)
    {
        if(is_int($h))
            $headers[$h]['Status'] = $v;
        else
        {
            if(is_string($v))
                $headers[0][$h] = $v;
            else
                foreach($v as $x => $y)
                    $headers[$x][$h] = $y;
        }

    }

    // Restore previous default context and return
    stream_context_set_default($prev);
    return $headers;
}

Example response:

<?php get_head('http://geekality.net');

array (size=2)
  0 =>
    array (size=8)
      'Status' => string 'HTTP/1.1 301 Moved Permanently' (length=30)
      'Date' => string 'Mon, 06 Feb 2017 01:20:48 GMT' (length=29)
      'Server' => string 'Apache' (length=6)
      'Location' => string 'http://www.geekality.net/' (length=25)
      'Vary' => string 'Accept-Encoding' (length=15)
      'Connection' => string 'close' (length=5)
      'Content-Type' => string 'text/html; charset=iso-8859-1' (length=29)
      'Link' => string '<http://www.geekality.net/wp-json/>; rel="https://api.w.org/"' (length=61)
  1 =>
    array (size=6)
      'Date' => string 'Mon, 06 Feb 2017 01:20:48 GMT' (length=29)
      'Server' => string 'Apache' (length=6)
      'Vary' => string 'Accept-Encoding' (length=15)
      'Connection' => string 'close' (length=5)
      'Content-Type' => string 'text/html; charset=UTF-8' (length=24)
      'Status' => string 'HTTP/1.1 200 OK' (length=15)

Example response without my cleanup:

<?php get_head('http://geekality.net');

array (size=9)
  0 => string 'HTTP/1.1 301 Moved Permanently' (length=30)
  'Date' =>
    array (size=2)
      0 => string 'Mon, 06 Feb 2017 01:14:00 GMT' (length=29)
      1 => string 'Mon, 06 Feb 2017 01:14:01 GMT' (length=29)
  'Server' =>
    array (size=2)
      0 => string 'Apache' (length=6)
      1 => string 'Apache' (length=6)
  'Location' => string 'http://www.geekality.net/' (length=25)
  'Vary' =>
    array (size=2)
      0 => string 'Accept-Encoding' (length=15)
      1 => string 'Accept-Encoding' (length=15)
  'Connection' =>
    array (size=2)
      0 => string 'close' (length=5)
      1 => string 'close' (length=5)
  'Content-Type' =>
    array (size=2)
      0 => string 'text/html; charset=iso-8859-1' (length=29)
      1 => string 'text/html; charset=UTF-8' (length=24)
  1 => string 'HTTP/1.1 200 OK' (length=15)
  'Link' => string '<http://www.geekality.net/wp-json/>; rel="https://api.w.org/"' (length=61)