Engineer as a Lifestyle @tenkoma

What We Find Changes Who We Become -- Peter Morville著『アンビエント・ファインダビリティ 』

Set::sort()で値が消える - array_unique in CakePHP

追記 2009/03/01 20:40

ソースにすこし手を加えたところ消えなくなったので、検証中…

追記 2009/03/01 22:07

Debian(squeeze/testing)にソースからビルドして入れたところ、やっぱり消えました。

array_unique関数がPHP5.2.9から後方互換性を失いました - hnwの日記
はてなブックマーク - array_unique関数がPHP5.2.9から後方互換性を失いました - hnwの日記

heavenshell php マジかよ。自分じゃほぼ使わないけど、FW とかライブラリで使ってるだろこれ | grep -r "array_unique" library/Zend | wc -l → 21 | zf じゃ 21 箇所で使ってる模様

hnw symfonyだとsfToolkit::arrayDeepMerge()が2配列のキーをarray_mergeしたものをarray_uniqueしてますな。どうみても影響があります。本当に(ry

CakePHPでは?

% cd ~/local/lib
% grep -r "array_unique" ./cakephp1.2.1.8004 | wc -l
      13
% grep -r "array_unique" ./cakephp1.2.1.8004
./cakephp1.2.1.8004/cake/console/cake.php:              $this->shellPaths = array_values(array_unique(array_merge($paths, Configure::read('shellPaths'))));
./cakephp1.2.1.8004/cake/console/libs/tasks/extract.php:                                $storage[$file][1] = array_unique(array_merge($storage[$file][1], $fileList));
./cakephp1.2.1.8004/cake/console/libs/tasks/extract.php:                                @$output[$this->__filename][1] = array_unique(array_merge($output[$this->__filename][1], $content[1]));
./cakephp1.2.1.8004/cake/libs/controller/components/security.php:                       $fieldList += array_unique($multi);
./cakephp1.2.1.8004/cake/libs/model/behaviors/containable.php:          $query['fields'] = array_unique($query['fields']);
./cakephp1.2.1.8004/cake/libs/model/behaviors/containable.php:                                  $Model->{$type}[$dependency]['fields'] = array_unique(array_merge($Model->{$type}[$dependency]['fields'], $innerFields));
./cakephp1.2.1.8004/cake/libs/model/behaviors/containable.php:          return array_unique($fields);
./cakephp1.2.1.8004/cake/libs/model/datasources/dbo_source.php:                                 'fields' => array_unique($queryData['fields']),
./cakephp1.2.1.8004/cake/libs/model/datasources/dbo_source.php:                                 'fields' => array_unique($assocData['fields']),
./cakephp1.2.1.8004/cake/libs/model/datasources/dbo_source.php:         return array_unique($fields);
./cakephp1.2.1.8004/cake/libs/model/model.php:                          $ids = array_unique(Set::extract('/' . $this->alias . '/parent_id', $return));
./cakephp1.2.1.8004/cake/libs/model/model.php:                  return array($with, array_unique(array_merge($assoc[$with], $keys)));
./cakephp1.2.1.8004/cake/libs/set.php:          $keys = array_unique($keys);

Set::sortで消える

grepしただけで影響がありそうだった、Setクラスを調べてみました。
Set::sort()でソートすると、値が消えるサンプルを作りました。

sample_controller.php

(修正 Set::sortの第二引数を'{[a-z0-9]+}.Person.value'→'{[a-zA-Z0-9]+}.Person.value'に修正)

<?php
class SampleController extends AppController
{
    var $uses = array();

    function index()
    {
        $test = array(
            '1e1' => array('Person' => array('value' => '1')),
            '10' => array('Person' => array('value' => '2')),
        );
        debug(Set::sort($test, '{[a-zA-Z0-9]+}.Person.value', 'desc'));
    }
}
php5.2.6 on debian(squeeze/testing)での実行結果
app/controllers/sample_controller.php (line 8)

5.2.6-3

app/controllers/sample_controller.php (line 13)

Array
(
    [0] => Array
        (
            [Person] => Array
                (
                    [value] => 2
                )

        )

    [1] => Array
        (
            [Person] => Array
                (
                    [value] => 1
                )

        )

)
php5.2.9 on Mac OS X 10.5 (Macports)での実行結果
app/controllers/sample_controller.php (line 8)

5.2.9

app/controllers/sample_controller.php (line 13)

Array
(
    [0] => Array
        (
            [Person] => Array
                (
                    [value] => 2
                )

        )

)

消えたッ!!
Set::sort()で、array_uniqueを使っている部分で第2引数にSORT_STRINGを付ければ、5.2.6の挙動と同じになりました。

消える組み合わせ

ひとつになりました。

// ... 略
        $test = array(
            '1e1' => array('Person' => array('value' => '1')),
            '10' => array('Person' => array('value' => '2')),
            '0x0a' => array('Person' => array('value' => '3')),
            '010' => array('Person' => array('value' => '4')),
        );
        debug(Set::sort($test, '{[a-zA-Z0-9]+}.Person.value', 'desc'));
// ... 略
Array
(
    [0] => Array
        (
            [Person] => Array
                (
                    [value] => 4
                )

        )

)

コメントより

hnw 浮動小数点数を使わなくても、'0'と'Z'とかでも消えると思います。連想配列のキーは整数にできる場合は数値になるので、今回のarray_uniqueの仕様変更が凶悪に効いてきます。

調べてみました。

// ... 略
        $test = array(
            '0' => array('Person' => array('value' => '5')),
            'Z' => array('Person' => array('value' => '6')),
        );
        debug(Set::sort($test, '{[a-zA-Z0-9]+}.Person.value', 'desc'));
// ... 略
Array
(
    [0] => Array
        (
            [Person] => Array
                (
                    [value] => 6
                )

        )

)

本当に消えた…なぜ?ちょっと調べてみました。
…わかった、と思います。
まず、上記のサンプルの '0'というキー配列を定義したあとvar_dumpしてみると、intになりました。(この時点で「えーっ」と思うのですが)
そして、キーだったものを比較すると、

var_dump('0' == "Z"); // false
var_dump(0 == "Z"); // true

0と'Z'が等しいとなります。この比較演算子の挙動はhttp://jp.php.net/manual/ja/language.operators.comparison.phpを見れば分かりますが、stringとintの比較はstring型を数値に変換してから比較します(int型とは限らない)。(string→数値の変換については…以下略)

まとめ

CakePHPでも影響ありました。
PHPって比較演算子で推移律が成立しない場合がある言語だったんですね。なんという上級者向き言語w