New Date/Time Support in MongoDB

In the past few months I have been working on adding time zone support to MongoDB's Aggregation Framework. This support brings in the timelib library that is also used in PHP and HHVM to do time zone calculations.

Time Zone Support for Date Extraction Operators

MongoDB's Aggregation Framework already has several operators to extract date/time information from a Date value. For example, $month would retrieve the month from a Date value. Considering we have the document:

{ _id: 'derickr', dateOfBirth: ISODate("1978-12-22T08:15:00Z") }

The following PHP code would extract the month 12 from the dateOfBirth field:

<?php
include 'vendor/autoload.php';

$collection = (new MongoDB\Client)->demo->birthdays;

$cursor = $collection->aggregate([
    [ '$project' => [
        'birthMonth' => [ '$month' => '$dateOfBirth' ]
    ] ]
]);

foreach ($cursor as $person) {
    echo $person->birthMonth;
}

However, it was not possible to extract this information in the local time zone. So although my birth-time is given at 08:15:00, in reality it was 09:15 in the Netherlands.

The new functionality adds the timezone argument to the date extraction functions. Currently the operators only take a single value:

{ "$month" : ISODateValue }

Because it is not possible to add another argument to this, the syntax has been extended to:

{ "$month" : { "date" : ISODateValue } }

And then with the time zone argument added, it becomes:

{ "$month" : {
    "date" : ISODateValue,
    "timezone": timeZoneIdentifier
} }

For example:

{ $hour: {
    date: ISODate("1978-12-22T08:15:00Z"),
    timezone: "Europe/Amsterdam"
} }

Would return 09 (One hour later than UTC).

The timezone argument is optional, and must evaluate to either an Olson Time Zone Identifier such as Europe/London or America/New_York, or, an UTC offset string in the forms: +03, -0530, and +04:45. If you specify a timezone argument it means that the dateString that you provided will be interpreted as it was in that time zone.

In PHP, this example extracts the hour, and expresses the value in the Europe/Amsterdam time zone:

<?php
include 'vendor/autoload.php';

$collection = (new MongoDB\Client)->demo->birthdays;

$cursor = $collection->aggregate([
    [ '$project' => [
        'birthHour' => [
            '$hour' => [
                "date" => '$dateOfBirth',
                "timezone" => "Europe/Amsterdam",
            ]
        ]
    ] ]
]);

foreach ($cursor as $person) {
    echo $person->birthHour, "\n";
}

The $dateToParts Operator

Extracting a single value from a date can easily be done with one of the Date Extraction operators, but it becomes cumbersome to do this for all the fields one by one, especially when considering the application of time zones as well:

{ "$project" : {
    "year": { "$year" : { "date" : '$dob', "timezone" : "Europe/Amsterdam" } },
    "month": { "$month" : { "date" : '$dob', "timezone" : "Europe/Amsterdam" } },
    "day": { "$day" : { "date" : '$dob', "timezone" : "Europe/Amsterdam" } },
    "hour": { "$hour" : { "date" : '$dob', "timezone" : "Europe/Amsterdam" } },
    "minute": { "$minute" : { "date" : '$dob', "timezone" : "Europe/Amsterdam" } },
} }

The new $dateToParts operator simplifies having multiple single date value extraction operators into a single one. Its syntax is:

{ "$project" : {
    "parts" : {
        "$dateToParts" : {
            "date" : ISODateValue,
            "timezone" : timeZoneIdentifier,
            "iso8601" : boolean
        }
    }
} }

The timezone argument is optional, and is interpreted in the same as the timezone argument in the Date Extraction functions as explained above.

The result of the operator is a sub-document with the broken down parts, expressed in the (optionally) given time zone:

"parts" : {
    "year" : 1978, "month" : 12, "day" : 22,
    "hour" : 9, "minute" : 15, "second" : 0, "millisecond" : 0
}

$dateToParts also supports a third boolean argument, iso8601. If set to true, instead of year, month, and day, it returns the ISO 8601 isoYear, isoWeekYear, and isoDayOfWeek fields representing an ISO Week Date. With the same date, the example is represented as:

"parts" : {
    "isoYear" : 1978, "isoWeekYear" : 51, "isoDayOfWeek" : 5,
    "hour" : 9, "minute" : 15, "second" : 0, "millisecond" : 0
}

In PHP:

<?php
include 'vendor/autoload.php';

$collection = (new MongoDB\Client)->demo->birthdays;

$cursor = $collection->aggregate([
    [ '$project' => [
        'parts' => [
            '$dateToParts' => [
                "date" => '$dateOfBirth',
                "timezone" => "Europe/Amsterdam",
            ]
        ]
    ] ]
]);

foreach ($cursor as $person) {
    var_dump( $person->parts );
}

Which outputs, with formatting:

class MongoDB\Model\BSONDocument#5 (1) {
  private $storage =>
  array(7) {
    'year' => int(1978)
    'month' => int(12)
    'day' => int(22)
    'hour' => int(9)
    'minute' => int(15)
    'second' => int(0)
    'millisecond' => int(0)
  }
}

The $dateFromParts Operator

The new $dateFromParts operator does the opposite of the $dateToParts operator and constructs a new Date value from its constituent parts, with the possibility of interpreting the given values in a different time zone.

Its syntax is either:

{ "$project" : {
    "date" : {
        "$dateFromParts": {
            "year" : yearExpression,
            "month" : monthExpression,
            "day" : dayExpression,
            "hour" : hourExpression,
            "minute" : minuteExpression,
            "second" : secondExpression,
            "millisecond" : millisecondExpression,
            "timezone" : timezoneExpression
        }
    }
} }

or:

{ "$project" : {
    "date" : {
        "$dateFromParts": {
            "isoYear" : isoYearExpression,
            "isoWeekYear" : isoWeekYearExpression,
            "isoDayOfWeek" : isoDayOfWeekExpression,
            "hour" : hourExpression,
            "minute" : minuteExpression,
            "second" : secondExpression,
            "millisecond" : millisecondExpression,
            "timezone" : timezoneExpression
        }
    }
} }

Each argument's expression needs to evaluate to a number. This means the source can be either double, NumberInt, NumberLong, or Decimal. Decimal and double values are only supported if they convert to a NumberLong without any data loss.

Every argument is optional, except for year or isoYear, depending on which variant is used. If month, day, isoWeekYear, or isoDayOfWeek are not given, they default to 1. The hour, minute, second and millisecond values default to 0 if not present.

The timezone argument is interpreted in the same as the timezone argument in the Date Extraction functions as explained above.

In PHP, an example looks like:

<?php
include 'vendor/autoload.php';

$collection = (new MongoDB\Client)->demo->birthdays;

$cursor = $collection->aggregate([
    [ '$project' => [
        'date' => [
            '$dateFromParts' => [
                "year" => 1978, "month" => 12, "day" => 22,
                "hour" => 9, "minute" => 15, "second" => 0,
                "millisecond" => 0,
                "timezone" => "Europe/Amsterdam",
            ]
        ]
    ] ]
]);

foreach ($cursor as $person) {
    var_dump( $person->date->toDateTime() );
}

Which outputs:

class DateTime#12 (3) {
  public $date => string(26) "1978-12-22 08:15:00.000000"
  public $timezone_type => int(1)
  public $timezone => string(6) "+00:00"
}

Changes to the $dateToString Operator

The $dateToString operator is extended with the timezone argument. Its full new syntax is now:

{ $dateToString: {
    format: formatString,
    date: dateExpression,
    timezone: timeZoneIdentifier
} }

The timezone argument is optional. If present, it formats the string according to the given time zone, otherwise it uses UTC.

The $dateToString format arguments have also been expanded. With the addition of the timezone argument came the %z and %Z format specifiers:

%z

The +hhmm or -hhmm numeric time zone as a string (that is, the hour and minute offset from UTC). Example: +0445, -0500

%Z

The minutes offset from UTC as a number. Example (following the +0445 and -0500 from %z): +285, -300

Once SERVER-29627 gets merged, the following new format specifiers will also be available:

%a

The abbreviated English name of the day of the week.

%b

The abbreviated English name of the month.

%e

The day of the month as a decimal number, but unlike %d, pre-padded with space instead of a 0.

An example of this in PHP:

<?php
include 'vendor/autoload.php';

$collection = (new MongoDB\Client)->demo->birthdays;

$cursor = $collection->aggregate([
    [ '$project' => [
        'date' => [
            '$dateToString' => [
                'date' => '$dateOfBirth',
                'format' => '%Y-%m-%d %H:%M:%S %z',
                'timezone' => 'Australia/Sydney',
            ]
        ]
    ] ]
]);

foreach ($cursor as $person) {
    echo $person->date;
}

Which outputs:

1978-12-22 19:15:00 +1100

The $dateFromString Operator

Analogous to PHP's DateTimeImmutable constructor, this operator can be used to create a Date value out of a string. It has the following syntax:

{ "$dateFromString": {
    "dateString": dateString,
    "timezone": timeZoneIdentifier
} }

The dateString could be anything like:

  • 2017-08-04T17:02:51Z

  • August 4, 2017 17:10:27.812+0100

In fact, it will accept everything that PHP's DateTimeImmutable constructor accepts as under the hood, it uses the same library. MongoDB enforces though that it is an absolute date/time string.

The timezone argument is optional, and is interpreted in the same as the timezone argument in the Date Extraction functions as explained above.

For example:

{ $dateFromString: {
    dateString: "2017-08-04T17:06:41.113",
    timezone: "Europe/London"
} }

Would mean 17:06 local time in London, or 16:06 in UTC (as London right now is at UTC+1).

It is not allowed to specify a time zone through the dateString (such as the ending Z or +0400) and also specify a time zone through the timezone argument. In that case, an exception is thrown.

In PHP, this looks like:

<?php
include 'vendor/autoload.php';

$collection = (new MongoDB\Client)->demo->birthdays;

$cursor = $collection->aggregate([
    [ '$project' => [
        'date' => [
            '$dateFromString' => [
                "dateString" => 'August 8th, 2017. 14:14:40',
                "timezone" => "Europe/Amsterdam",
            ]
        ]
    ] ]
]);

foreach ($cursor as $person) {
    var_dump( $person->date->toDateTime() );
}

Which outputs:

class DateTime#12 (3) {
  public $date =>
  string(26) "2017-08-08 12:14:40.000000"
  public $timezone_type =>
  int(1)
  public $timezone =>
  string(6) "+00:00"
}

As you can see, the time zone information is lost when the data is transferred between MongoDB and PHP as the BSON DateTime data type does not carry this information.

Using Date Expressions in $match

From MongoDB 3.5.12, it is also possible to use the new date expressions (and other expressions) in the $match pipeline operator. For example, in order to find all the documents before June 17th, 2017 in the New York time zone:

db.dates.aggregate( [
    { $match: {
        date: { $gte: { $expr: {
            $dateFromString: {
                dateString: "June 17th, 2017",
                timezone: "America/New_York"
            }
        } } }
    } }
] );

Or from PHP:

<?php
include 'vendor/autoload.php';

$collection = (new MongoDB\Client)->demo->dates;

$date = "June 17th, 2017";

$cursor = $collection->aggregate( [
    [ '$match' => [
        'date' => [ '$gte' => [ '$expr' => [
            '$dateFromString' => [
                "dateString" => [ '$literal' => $date ],
                "timezone" => "America/New_York",
            ]
        ] ] ]
    ] ]
]);

foreach ($cursor as $person) {
    var_dump( $person->date->toDateTime() );
}

Please note the use of the $literal operator here, which should be used for any user input that might be able to sneak in an expression into the value.

Notes

The time zone support is currently only available in a development release of MongoDB, and should be considered experimental. It is likely that some of it will still change. In particular:

  • Before MongoDB 3.5.12, the argument millisecond to dateFromParts is incorrectly spelled milliseconds.

  • Until SERVER-30547 is resolved, $dateFromParts does not accept an sub-document as argument, and instead requires each single field to be specified.

  • Until SERVER-30523 is resolved, the field values to dateFromParts can not underflow or overflow their expected range. For example, the day field's value needs to be in the range 1..31 and the hour field's value needs to be in the range 0..23.

Shortlink

This article has a short URL available: https://drck.me/mongotimelib-ddg

Comments

No comments yet

Detecting Problems With -fsanitize

In the past few months I have been working on adding time zone support to MongoDB's Aggregation Framework. This support brings in the timelib library that is also used in PHP and HHVM to do time zone calculations. One of the stages in our workflow before we commit code to master, is to put our patches up onto our continuous integration platform Evergreen, where tests are run against multiple platforms. You expect the usual Ubuntu, RHEL and Windows platforms, but we also run on more esoteric platforms like s390. We also define a few special platforms that run tests in an environment where the code has been compiled in a special mode to test for undefined behaviour in the C language, and memory leaks.

One of the issues that this found (quite quickly) in timelib, was:

src/third_party/timelib-2017.05beta3/parse_tz.c:152:16: runtime error:
  left shift of 128 by 24 places cannot be represented in type 'int'

Which referred to the following code:

buffer[i] = timelib_conv_int(buffer[i]);

timelib_conv_int is a macro defined as:

#define timelib_conv_int(l) ((l & 0x000000ff) << 24) + \
        ((l & 0x0000ff00) << 8) + ((l & 0x00ff0000) >> 8) + \
        ((l & 0xff000000) >> 24)

The sanitiser stumbled over some of the data in the Olson database where we attempted to shift the unsigned integer 128 left by 24 positions into an signed integer, which of course can not represent this value. Thanks to the sanitizer, beta4 has this problem now fixed.

As part of the fix, I investigated how our tests managed to figure out that there was undefined behaviour. It appeared that GCC and clang have a specific flag to enable this debugging tool. It is as simple as adding -fsanitize=undefined to your build flags, which is what timelib in its standalone Makefile now includes.

One of the things that is tricky is that when writing C++, I tend to use many C-isms as that is what I have been working in for so long. And the opposite is true to. C++-isms are sometimes used when dealing with the timelib library which is written in C. One of these issues created a memory leak (fixed through this patch), as a structure was not allocated on the heap, but on the stack. This structure (timelib_time*) sometimes contains an extra string (for the time zone abbreviate) that needs to be freed if set.

This memory leak was also discovered by a sanitizer flag, but this time it was -fsanitize=address. This flag adds code to the compiled binary to test for memory leaks, overflows, etc.—not to dissimilar as to what Valgrind (I wrote about that before) provides. After adding this flag to the default build rules for timelib, it quickly found a few other memory leaks in some test and example files which I then addressed.

So there we have it, two new GCC and Clang flags that I did not know about. I now always compile timelib, as well as my local MongoDB test builds with these two flags. It certainly slows down execution, but that's a cheap price to pay to prevent bugs from making it into production.

Shortlink

This article has a short URL available: https://drck.me/san-dcf

Comments

No comments yet

HHVM and MongoDB

At the start of 2015 we began work on an HHVM driver for MongoDB, as part of our project to renew our PHP driver. Back then, HHVM was in its ascendancy and outperforming PHP 5.6 two to one. With such a huge performance difference it was reasonable to assume that many users would be switching over.

Around the start of 2016 I wrote a series of blog posts when we released the brand new drivers for PHP and HHVM. However, by then, PHP 7.0 had been released. PHP 7's performance is on par with HHVM making it less useful for users to move from PHP to HHVM. HHVM still offers a more strongly typed PHP syntax through Hack, and some other features, but its main attraction, speed, was mostly gone.

Writing an extension for HHVM is very different than doing so for PHP. PHP extensions, of which there are plenty, are written in C. HHVM extensions are written in C++, with very few third party extensions existing. At the same time, although PHP's APIs are not particularly well documented, there is a large group of people to ask for help. PHP also has a clearly defined internal API which is stable across minor versions. HHVM does not have this, and APIs kept changing often, although it wasn't always clear whether we were using an internal API.

Writing an HHVM extension is a very different experience for extension developers compared to PHP extensions. There is even less documentation, and virtually no third party extensions to look at for "inspiration". At the same time, it was much harder to get help from the developers, and much harder to debug as HHVM is many times more complex than PHP.

With PHP 7 released, we saw very little use of the HHVM driver for MongoDB. Some months ago I did a twitter poll, where very few people were indicating that they were using HHVM—and even if they were, they would likely not choose to switch to HHVM given the current climate.

Some of the feedback on the poll was not very assuring either:

With few users, frequent API breaks, and curious bugs we came to the conclusion that supporting the HHVM driver for MongoDB no longer makes good use of our engineering time. With Doctrine and Symfony 4 also no longer supporting HHVM, we have decided to discontinue the MongoDB driver for HHVM.

If anyone is interested in assuming ownership of the HHVM driver, please drop me a line and we can discuss the process in more detail.

Shortlink

This article has a short URL available: https://drck.me/hhvm-mongo-da6

Comments

No comments yet

15 years of Xdebug

This article was going to be about some upcoming features in the 2.6 release. Or rather, I was hoping to announce at least a beta release of Xdebug 2.6. Unfortunately, I couldn't find enough time to work on all the issues that I wanted, although I've made a little progress.

What I can write about, is a little mystery. About 3 weeks ago, I got a mysterious invitation to meet up with James Titcumb, right outside my (and James's!) favourite whisky shop in London. And that I must bring some carrying capacity:

At that time I had had put together that with Xdebug's 15th anniversary looming, and me having an Amazon wishlist full of whisky, James would be kind enough to buy me something less "standard".

I however had not quite expected what actually happened. During the day of meeting, I saw some tweets going on about a little (secret) fundraiser. Apparently I wasn't to know, but it is difficult to keep things a secret I suppose. In any case, because I had thought it'd have something to do with meeting James later that day, I didn't really read much of it, as it would likely spoil a surprise.

And what a surprise it was!

So I show up at 5, and there is James with his phone trying to figure out how Periscope works. We go in and the manager, has a story to tell about 8 quite amazing whiskies, which I then get to take home. With amazing, I mean, amazing and special and rare whiskies from closed down distilleries, and a few more approachable whiskies. I quickly realized that they are not, erm, cheap either:

  • Balvenie 21 Portwood

  • Dalmore King Alexander

  • Dallas Dhu 27

  • Springbank 25 - 2017 edition

  • Caperdonich 20 - (Demolished Distillery) 2016 release

  • Glenlochy 1979 - (Demolished Distillery) Rare Old Label 2016

  • St.Magdalane 26 - (Demolished Distillery) Rare Archive bottle from 2006

  • Banff 1974 18 year old 40% - (Demolished Distillery) Rare Archive bottle from 1992 Gordon & Macphails

I have tried the Balvenie Portwood 21 and Dalmore King Alexander before, but certainly not the others!

So yeah, after borrowing a suitcase I managed to get these great bottles home, and while doing so, James explains what hat happened. PHP's "godfather", Cal Evans, had originally intended to raise money to buy the most expensive bottle from his local whisk(e)y shop - at 4699 USD. James (luckily) managed to convince him that in the whisky world, price doesn't always equal quality. There is a bit of a limit at perhaps £125 for "normal" bottles, but of course quite a bit more for "rare" whiskies. They managed to raise slightly more, through the generous donation of many people and companies that find Xdebug useful. I saw the list, and there were many lovely messages in there as well, a few of them I am including here:

"XDebug is hugely important for our team. Thank you for all you have done!"

"var_dump($scotch);"

"Thanks Derick! Such a critical part of day-to-day PHP dev life =)"

"Xdebug has helped me solving numerous bugs, thanks!"

"Because every good developer knows that great code begins with great debugging tools!"

"xdebug helps me to not drink... ashnazg"

"Thank you for your efforts in the community!"

"Xdebug has made my development life immeasurably easier over the last 15 years. Here's to the next 15, and a massive thank you! :-)"

"For one of the most used and usefully tools in a professionals day to day live beside a good editor."

"It's people like you that make this such an amazing community! Thank you for giving yourself so freely for so long!"

"I can barely remember what coding was like before discovering XDebug. Thank you Derick for making our lives easier!"

Thanks for these lovely messages, and thanks for donating to my whisky fund Aaron Saray, Accent Interactive, Adam Culp, Adam Kammeyer, Adrián Cárdenas, Alain Schlesser, Alex Ross, Alexander Marinov, Andreas Heigl, Andrew Caya, Andrew Millington, Antonis Pavlakis, Barry Hughes, Bart Reunes, Ben Ramsey, Bernhard Breytenbach, Bill Condo, Boone Gorges, Boyan Djumakov, Boyan Yordanov, Chris Brookins, Chris Hartjes, Chris Sherry, Chris Spruck, Chuck Burgess, Code4Hire Kft, Cristiano Diniz da Silva, Damien Seguy, Daniel Abernathy, Dave Hall, David Alger, David Lundgren, Diana Espino, Doug Johnson, Dougal Campbell, Enrico Zimuel, Eric Hogue, Ferenc Kovács, Fran Novo, Frank de Jonge, Frederic Dewinne, Freek Van der Herten, Gilbert Pellegrom, Goran Mitrovic, Gordon Forsythe, Guillaume Rossolini, Iain Poulson, Ian H, Ian Littman, J.T. Grimes, Jake Smith, Jakub Gadkowski, James LaChance, James Titcumb, Jeff Carouth, Jeff Kolesnikowicz, Jeff Rupert, Jeremy Emery, Jeremy Lindblom, Jeroen Boersma, Jeroen de Jong, JetBrains sro, Joey Fowler, Josh Butts, Josh Holmes, Joshua Thijssen, Juliette Reinders Folmer, Kara Ferguson, Kathryn Reeve, Ken Sherman, Kevin Schroeder, Lance Cleveland, Laura Folco, Liam Wiltshire, Lucas van Lierop, Luke Stokes, Mark Baker, Matt Trask, Matthew Weier O'Phinney, Max Griffin, Merlijn Tishauser, Michael Babker, Michael Butler, Michael Dyrynda, Michael Moussa, Michael Pearson, Michael Stowe, Michael Williams, Mihail Irintchev, Milan Popovic, Modern Tribe, Nate Ritter, Navarr Barnier, Nikolay Ignatov, Nils Preuss, Noah Heck, Omni Adams, Paul McGrane, Paul Sohier, Paul Yasi, Peter Breuls, Pádraic Brady, Rafael Dohms, Rich Sage, Richard Bairwell, Richard Hagen, Rob Allen, Robert Basic, Robert Landers, Rodrigo Capilé, Russell Barnhart, Ryan Weaver and Leanna Pelham, Samantha Quiñones, Sammy Powers, Sandy Smith, Scott Arciszewski, Sebastian Feldmann, Shaun Hare, SitePoint PHP Channel, Stefan Koopmanschap, Stephan Hochdörfer, Steve Grunwell, Steven Wade, Svetlozar Stoyanov, Team Enrise, Tim Stamp, Tom Cruickshank, Tom De Wit, Toni Vega, Ulf Wandschneider, Willem-Jan Zijderveld, Wim Godden, Youri Thielen, Zeke Farwell, and the anonymous donors!*

What's next?

I'll be publishing the tasting notes for the whiskies on the https://dram.io — one of the few places where I actually use Xdebug myself. I might not open all of them (yet) though.

And on the Xdebug front, there are plenty of bugs to fix, features to add for Xdebug 2.6, and undoubtedly Dmitry will be "breaking" some things in PHP 7.2 that I need to support in Xdebug as well.

Slàinte!

Shortlink

This article has a short URL available: https://drck.me/xdebug-10-d9k

Comments

I'm sorry to have missed hearing about the donation call, 'cause I guess that means I won't be getting any tastings!

Just wanted to say thank you :)

Cheers brother, mad respect and thanks for xdebug!!!

I also just wanted to say super thanks for this amazing tool called Xdebug. Enjoy the well deserved whiskeys ;-)

Thanks for XDebug. Makes life a lot more easier and enjoyable. I use it all the time to learn new stuff under the hood.

Good Bye PHP 5

A few days ago I merged a patch into Xdebug that removes support for PHP 5 in Xdebug's master branch on GitHub. Maintaining PHP 5 and PHP 7 support in one code base is not particularly easy, and even more complicated for something like Xdebug, with its deep interactions with PHP's internals.

As PHP 5.6's active support has ended on December 31st, I also felt it no longer needed to support PHP 5 with Xdebug any more. It saves more than 5000 lines of code:

Many people people were quite positive about that:

Others were less keen:

Removing PHP 5 support from Xdebug's master branch does not mean that Xdebug suddenly stops working for PHP 5 installations. Xdebug 2.5, which was recently released supports PHP 5.5 and 5.6, and is not going to go away.

Right now, Xdebug will no longer receive new features in the branch that also supports PHP 5. New features will only go into master (to become Xdebug 2.6). However, Xdebug 2.5 continues to receive bug fixes until Xdebug 2.6 comes out.

Once Xdebug 2.6 comes out, the Xdebug 2.5 branch will no longer get bug fixes, and hence support for PHP 5 goes away. That still does not mean that you can no longer use Xdebug with PHP 5. The releases of the 2.5 branch will still be available.

On the positive side, not having to implement lots of code twice, also means that new features can be added faster, as less work is required. Xdebug 2.6 has already have some new features lined up.

Shortlink

This article has a short URL available: https://drck.me/byephp5-d4v

Comments

No comments yet

Life Line