[PHP] MySQLをつかったDateTimeの補完

### 動機
PHPのDateTime型において、1ヶ月の加減算は仕様により都合がわるい。
[DateTime::add](http://php.net/manual/ja/datetime.add.php)
[DateTime::sub](http://php.net/manual/ja/datetime.sub.php)


```php:test_add.php
<?php
$datetime = new DateTime('2015-03-31');
var_dump($datetime);

$datetime->add(new DateInterval('P1M'));
var_dump($datetime);

```

```shell-session
$ php test_add.php
object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-03-31 00:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-05-01 00:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}
```

``3/31``の1ヶ月後は``4/30``になってほしい

```php:test_sub.php
<?php
$datetime = new DateTime('2015-03-31');
var_dump($datetime);

$datetime->sub(new DateInterval('P1M'));
var_dump($datetime);
```

```shell-session
$ php test_sub.php
object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-03-31 00:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-03-03 00:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}
```

``3/31``の1ヶ月前は``2/28``になってほしい

そこで、大変野蛮なやり方であるが、``MySQL``くんの力を借りてみる試験を行った。

```php:MyDateTime.class.php
<?php
class MyDateTime
{
    private static $_dbh;
    private static $_datetime;

    private static $_dbInfo = array(
        'dbhost'  => 'dbhost',
        'dbuser'  => 'dbuser',
        'dbpass'  => 'dbpass',
        'dbname'  => '',
        'charset' => 'utf8',
        'driver'  => 'mysql',
        'dbport'  => '1234',
    );


    /**
     * create instance guard
     *
     * @return void
     */
    private function __construct(){}


    /**
     * func doc comment
     *
     * @return void
     */
    public static function create($_string='')
    {
        self::_init($_string);
        return self::$_datetime;
    }


    /**
     * https://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_date-add
     *
     * @return void
     */
    public static function add($_string='')
    {
        if (! empty($_string)) {
            $query = "SELECT DATE_ADD('".self::$_datetime->format('Y-m-d H:i:s')."', INTERVAL ".$_string.") AS datetime";
            return self::_addsub($query);
        }
    }


    /**
     * https://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_date-add
     *
     * @return void
     */
    public static function sub($_string='')
    {
        if (! empty($_string)) {
            $query = "SELECT DATE_SUB('".self::$_datetime->format('Y-m-d H:i:s')."', INTERVAL ".$_string.") AS datetime";
            return self::_addsub($query);
        }
    }


    /**
     * func doc comment
     *
     * @return void
     */
    private static function _addsub($query)
    {
        $rslt = self::$_dbh->query($query);
        $row = $rslt->fetch_assoc();
        $rslt->close();
        return DateTime::createFromFormat('Y-m-d H:i:s', $row['datetime']);
    }


    /**
     * function doc comment
     *
     * @return void
     */
    private static function _connect()
    {
        if (self::$_dbh === null) {
            self::$_dbh = new mysqli(
                self::$_dbInfo['dbhost'],
                self::$_dbInfo['dbuser'],
                self::$_dbInfo['dbpass'],
                self::$_dbInfo['dbname']
            );
            self::$_dbh->set_charset(self::$_dbInfo['charset']);
        }
    }


    /**
     * func doc comment
     *
     * @return void
     */
    private static function _init($_string)
    {
        self::_connect();

        if (empty($_string)) {
            $query = "SELECT NOW() AS datetime";
        } else {
            $query = "SELECT DATE_FORMAT('".$_string."', '%Y-%m-%d %H:%i:%s') AS datetime";
        }

        $rslt = self::$_dbh->query($query);
        $row  = $rslt->fetch_assoc();
        $rslt->close();

        self::$_datetime = DateTime::createFromFormat('Y-m-d H:i:s', $row['datetime']);
    }
}

```


```php:test.php
<?php
require_once 'MyDateTime.class.php';

// 指定なし
$datetime = MyDateTime::create();
var_dump($datetime);

// 指定あり
$datetime = MyDateTime::create('2015-03-31');
var_dump($datetime);

// 加算
$dateadd = MyDateTime::add('1 MONTH');
var_dump($dateadd);

// 減算
$datesub = MyDateTime::sub('1 MONTH');
var_dump($datesub);
```



```shell-session
$ php test.php
object(DateTime)#4 (3) {
  ["date"]=>
  string(26) "2015-04-27 11:34:12.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}
object(DateTime)#3 (3) {
  ["date"]=>
  string(26) "2015-03-31 00:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}
object(DateTime)#1 (3) {
  ["date"]=>
  string(26) "2015-04-30 00:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}
object(DateTime)#5 (3) {
  ["date"]=>
  string(26) "2015-02-28 00:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(10) "Asia/Tokyo"
}
```

うまくいった。


### ご注意

**上記は試験スクリプトです。セキュリティ的パフォーマンス的使い勝手的な考慮は含まれていません。**


### 追記

``Carbon``というDateTime拡張ライブラリが、便利かも。

Official: [Carbon](http://carbon.nesbot.com/)
packagist: [nesbot/carbon](https://packagist.org/packages/nesbot/carbon)