古い PHP を使うときに問題となりそうな箇所

あるバージョンの PHP を想定して書かれたコードをより古いバージョンの PHP で動作させないといけなくなったときに、問題になりそうな箇所を挙げる。

PHP 7.3 → 7.4

  • 配列のスプレッド構文
    • $a = [1, 2]; $b = [0, ...$a, 3, 4];
  • 矢印関数(ショートクロージャ
    • array_map(fn($n) => $n * $n, $a);
  • クラスのプロパティに対する型宣言
    • class C { protected int $id; }

PHP 7.0 → 7.1

  • file_get_contents() の第4引数 $offset のデフォルト値が -1 から 0 に変更された

PHP 5.6 → 7.0

  • 関数のプリミティブ型の型宣言
    • function getUserName(int $userId): ?string
  • 無名関数の直接呼び出し
    • (function() { ... })()
  • date() 関数や DateTime::format() でのフォーマット文字 v (ミリ秒) の追加
  • Null 合体演算子
    • $user = $_GET['user'] ?? 'nobody'; // isset($_GET['user']) ? $_GET['user'] : 'nobody';

PHP 5.5 → 5.6

  • 定数式
    • const ONE = 1; const TWO = ONE * 2;
  • ... による可変個引数関数
    • function f($req, $opt = null, ...$params) { ... }
  • ... による引数のアンパック
    • $arr = [2, 3]; f(1, ...$arr); // = f(1, 2, 3)
  • 累乗演算子 ** および代入演算子 **=
  • use function および use const
  • php://input が2回以上オープンできるように
  • hash_equals() 関数の追加
  • 処理が厳格になったもの
    • json_decode() が JSON の仕様通りに true, false, null は小文字のみ受け付けるようになった
    • Mcrypt 関数が有効なキーと IV を与えない場合、失敗するようになった
    • cURL で @file 構文によってアップロードできないようになった。代わりに CURLFile を使う
  • DateTime
    • DateTimeImmutable::createFromMutable() の追加
  • GMP
  • PDO
    • PDO::pgsqlGetNotify(), PDO::pgsqlGetPid() の追加
  • Session
    • session_abort(), session_reset() の追加
  • Zip
    • ZipArchive::setPassword() の追加

PHP 5.4 → 5.5

  • ジェネレータが追加されました。 yield キーワードで利用します。
  • try-catch ブロックで finally が使えるようになりました。
  • 新しいパスワードハッシュ API … password_hash(), password_needs_rehash(), password_verify() など
  • foreach で list() を使って、ネストした配列を個別の変数に展開できるようになりました。
  • empty() が変数以外の任意の式に対応
  • array リテラルと string リテラルデリファレンス
  • ::class によるクラス名の解決
  • foreach が非スカラーのキーに対応
  • array_column() 関数の追加
  • DateTimeImmutable クラスの追加
  • GD の改良

PHP 5.3 → 5.4

  • トレイトのサポート
  • 配列の短縮構文
    • $a = [1, 2, 3]; // array(1, 2, 3)
  • 関数の返り値を変数に格納することなく foo()[0] のように各要素にアクセスできる
  • クラスのインスタンスを変数に格納することなく (new Foo)->bar() のようにメンバーにアクセスできる
  • クロージャ内で $this が使用できる
  • 2進数の整数リテラル 0b001001101

PHP 5.3.8 → 5.3.9

PHP 5.2 → 5.3

  • 名前空間 のサポートが追加 namespace foo;
  • 遅延静的束縛 のサポートが追加 static::bar()
  • ネイティブクロージャ (ラムダ/無名関数) のサポートが追加
    • $f = function(...) {...};
  • json_encode() - 引数 options が追加
  • json_decode() - 引数 depth が追加
  • DateTime::add(), diff(), sub() などが追加
  • class DateInterval, DatePeriol 追加
  • SQLite3 モジュール (class SQLite3)
  • 定数 __DIR__

PHP 5.1 → 5.2

  • SplFileObject::getFilename() は、PHP 5.2.1 以降ではファイルへの相対パスではなくファイル名のみを返すようになりました。
  • __toString() が、適切な場面で常にコールされるようになりました。
  • 拡張モジュール JSON
  • クラス DateTime, DateTimeZone
  • date() 関数でのフォーマット文字 u (マイクロ秒) の追加

DateTime オブジェクトのタイムゾーンの設定

タイムゾーンを特に指定しない場合、 date_default_timezone_get() の値が使われる。

<?php
$date = new DateTime;

$tz = $date->getTimezone();
$tz->getName(); // Asia/Tokyo
date_default_timezone_get(); // Asia/Tokyo

$date->getTimestamp(); // 1410130800
$date->format('Y/m/d H:i:s'); // 2014/09/08 08:00:00

@ + UNIX タイムスタンプで初期化した場合

DateTime クラスのコンストラクタに、'@' + UNIX タイムスタンプ という文字列を指定した場合、そのオブジェクトのタイムゾーンは強制的に UTC となる。

<?php
$date = new DateTime('@1410130800');

$tz = $date->getTimezone();
$tz->getName(); // +00:00
$date->getTimestamp(); // 1410130800
$date->format('Y/m/d H:i:s'); // 2014/09/07 23:00:00

一方 setTimestamp() でタイムスタンプを指定した場合は、そのオブジェクトが持っているタイムスタンプが受け継がれる。

<?php
$date = new DateTime;
$date->setTimestamp(1410130800);

$tz = $date->getTimezone();
$tz->getName(); // Asia/Tokyo
$date->getTimestamp(); // 1410130800
$date->format('Y/m/d H:i:s'); // 2014/09/08 08:00:00

ある地域の時刻を別の地域の時刻に変換する

変換元のタイムゾーンを指定してから時刻を表す文字列を解析し、変換先のタイムゾーンを指定して整形関数を使えばよい。

<?php
$date = new DateTime('2014/09/08 08:00:00', new DateTimeZone('Europe/London'));
$date->format('Y/m/d H:i:s'); // 2014/09/08 08:00:00

$date->setTimezone(new DateTimeZone('UTC'));
$date->format('Y/m/d H:i:s'); // 2014/09/08 07:00:00 (夏時間のためロンドンの時刻は UTC より 1 時間進んでいる)

【旧版】Google Analytics API を PHP から使う


2016年4月1日時点、API が更新されて使い方が変わっています。こちらの新しい記事をご覧ください。

目標

Google Analytics API を使って、人気のページのランキングを作る。

次の記事はバージョンの古い PHP Client を使っているため、最新版で動作するようにする。

サービスから API を使えるよう設定を行う

この記事を参考に第6節「Google Analytics のビュー ID をメモする」までの手順を行う。

この手順で作成するものは次の通り。

  • API プロジェクト(Facebook API では「アプリ」と呼ばれるものに相当)
  • OAuth のサービスアカウント
    • クライアント ID
    • メール アドレス
    • 秘密鍵
  • 上記 OAuth クライアントと紐づいた Google Analytics ユーザー

Google API PHP Client を配置する

次のページから src 以下のファイルをダウンロードする。Git の使い方に詳しくなければ「Download ZIP」からまとめてダウンロードするとよい。

require/include で読み込み可能なディレクトリに Google というディレクトリを作り、その中にダウンロードした src 以下のファイルを配置する。

API を使う

毎回 API への接続条件を書くのは大変なので次のようなクラスを作った。新しい PHP Client ではこの部分で使うクラス名や関数名が変更されている。

<?php
// my_google_analytics.php
require_once 'Google/Client.php';
require_once 'Google/Auth/AssertionCredentials.php';
require_once 'Google/Service/Analytics.php';

class MyGoogleAnalytics {

	// クライアント ID
	const CLIENT_ID = 'XXXXXXXX.apps.googleusercontent.com';

	// メール アドレス
	const SERVICE_ACCOUNT_NAME = 'XXXXXXXX@developer.gserviceaccount.com';

	// 秘密鍵のファイル名
	const KEY_FILE = 'XXXXXXXX-privatekey.p12';
	
	private static $client;
	private static $service;

	public static function get() {
		if (! isset(self::$client)) {
			self::createClient();
		}
		
		if (! isset(self::$service)) {
			self::$service = new Google_Service_Analytics(self::$client);
		}
		return self::$service;
	}

	private static function createClient() {
		self::$client = new Google_Client();
		self::$client->setApplicationName('Suzume Kawaii Application');

		self::$client->setClientId(self::CLIENT_ID);
		self::$client->setAssertionCredentials(new Google_Auth_AssertionCredentials(
			self::SERVICE_ACCOUNT_NAME,
			array('https://www.googleapis.com/auth/analytics.readonly'),
			file_get_contents(__DIR__ . '/' . self::KEY_FILE)
		));
	}
}

次のように MyGoogleAnalytics::get() を呼ぶだけですぐに API を呼び出すためのオブジェクトを生成できる。

<?php
require_once 'my_google_analytics.php';

$ga = MyGoogleAnalytics::get();
$result = $ga->data_ga->get(
	'ga:XXXXX', // XXXXX の部分は Analytics のビュー ID
	'2014-05-01', // 開始日
	'2014-05-31', // 終了日
	'ga:pageviews', // 主要指標 (metrics)
	array(
		'dimensions' => 'ga:pageTitle,ga:pagePath', // 副指標
		'sort' => '-ga:pageviews', // - を付けると降順ソート
		'max-results' => 10, // 取得件数
	));

print_r($result['rows']);

取得できるデータの種類

指標 metrics および dimension で指定できる項目はこのページで参照できる。

主な指標を挙げておく。

  • Metrics
    • ga:sessions セッション数
    • ga:users ユーザ数
    • ga:pageviews ページビュー
  • Dimensions
    • ga:userType 新規訪問/リピート訪問 (例: New Visitor, Returning Visitor)
    • ga:medium 参照元の種類 (例: organic, referral, (none))
    • ga:source 参照元 (例: google, yahoo, (direct), example.com)

デスクトップ/モバイル(スマートフォン)を判断するベストプラクティス

バイル環境では通信の帯域が限られているため、内容を絞り込んだコンテンツを提供するのが定石となっている。

その判定を行うのに現在最も有効な方法は、HTTP リクエストに含まれる User-Agent ヘッダを参照する方法だ。数多くのアクセス解析ツールでも、ユーザの環境を判断するのに User-Agent ヘッダが用いられている。

さて、環境ごとの User-Agent を調べるのは骨の折れる作業である。そこで巨人の肩を借りることにする。

WPtouch Mobile Plugin から拝借する

「モバイル向け」などのキーワードで検索するとまず目についたのが Wordpress 用のプラグイン WPtouch Mobile Plugin である。このプラグインも例にもれず User-Agent ヘッダを用いてユーザ環境を判断している。

見たところ有償版の WPtouch Pro も含めそれなりのユーザ数がありそうなので、この仕組みを拝借しよう。

このプラグインをダウンロードして展開すると、 wptouch/core/mobile-user-agents.phpスマートフォンの User-Agent に含まれる文字列が列挙されている。12項目とシンプルにまとまっていて、判定の負荷も少なそうだ。

<?php

/* Smartphones */
global $wptouch_smartphone_list;
$wptouch_smartphone_list = array(
	array( 'iPhone' ), 								// iPhone
	array( 'iPod', 'Mobile' ),						// iPod touch
	array( 'Android', 'Mobile' ), 					// Android devices
	array( 'Opera', 'Mini/7' ), 					// Opera Mini 7
	array( 'BB', 'Mobile Safari' ), 				// BB10 devices
...

判定コードは次のように書ける。

<?php

function is_mobile() {
	static $is_mobile;
	
	if (isset($is_mobile)) {
		return $is_mobile;
	}
	
	$useragent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
	
	foreach ($wptouch_smartphone_list as $substrs) {
		$matched = true;
		foreach ((array)$substrs as $substr) {
			$pattern = preg_quote($substr, '/');
			if (! preg_match("/\\b$pattern\\b/i", $useragent)) {
				$matched = false;
				break;
			}
		}
		if ($matched) {
			$is_mobile = true;
			break;
		}
	}
	return $is_mobile;
}

if (is_mobile()) { ... } // モバイル用のコード
else { ... } // デスクトップ用のコード

Mobile Detect

その名の通り Mobile Detect というライブラリもある。こちらはかなり詳細に判定しているようだ。

UTC時刻の文字列からローカル時刻を出力する

SQLite には日付型はないが、 CURRENT_TIMESTAMP で現在時刻を文字列として取得することができる。また、date などの関数で時刻の演算することもできる。

SELECT
  CURRENT_TIMESTAMP AS now
, datetime('now', 'start of month') AS first_day
, datetime('now', 'start of month', '+1 month', '-1 day') AS last_day
;

出力は次のようになる。

now => 2013-11-14 21:12:34
first_day => 2013-11-01 00:00:00
last_day => 2013-11-30 23:59:59

ただしここで出力される時刻は世界協定時 UTC に固定されているため、アプリケーション側でローカル時刻に変換する必要がある。

PHP の場合、この変換に PHP 5.2 から用意されている DateTime クラスを使うことができる。下記の例は UTC 時刻の文字列を受け取り、ローカル時刻の文字列を返す。

<?php
function formatLocalDatetime($utcstr) {
	static $UTC, $Local;
	if (! isset($UTC)) {
		$UTC = new DateTimeZone('UTC');
		$Local = new DateTimeZone(date_default_timezone_get());
	}

	try {
		$date = new DateTime($utcstr, $UTC);
		$date->setTimeZone($Local);
		return $date->format('Y/m/d H:i:s');
	}
	catch (Exception $e) {
		return '';
	}
}

date_default_timezone_set('Asia/Tokyo');
echo formatLocalDateTime('2013-11-14 21:12:34'); // => 2013-11-15 06:12:34

単に UNIX 時間を取得する場合は $date->getTimestamp() でよい。