This project is intended for gathering all those PHP strange behaviours I keep discovering from time to time, and share them with you. They're covered as unit tests to show them in code, but I'll try to keep this README updated to explain in detail every curiosity. I hope some of them will be useful to you.
I discovered this weird thing while trying to do a loop like this:
for ($ch = 'a'; $ch <= 'z'; $ch++) {
// do something with $ch
}
I expected that loop to run 26 times, with $ch taking values from 'a' to 'z', both included. Well, that's not what happens. Instead it runs 676 times, with $ch taking the following values:
a b c d e f g h i j k l m n o p q r s t u v w x y z aa ab ac ad ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp bq br bs bt bu bv bw bx by bz ca cb cc cd ce cf cg ch ci cj ck cl cm cn co cp cq cr cs ct cu cv cw cx cy cz da db dc dd de df dg dh di dj dk dl dm dn do dp dq dr ds dt du dv dw dx dy dz ea eb ec ed ee ef eg eh ei ej ek el em en eo ep eq er es et eu ev ew ex ey ez fa fb fc fd fe ff fg fh fi fj fk fl fm fn fo fp fq fr fs ft fu fv fw fx fy fz ga gb gc gd ge gf gg gh gi gj gk gl gm gn go gp gq gr gs gt gu gv gw gx gy gz ha hb hc hd he hf hg hh hi hj hk hl hm hn ho hp hq hr hs ht hu hv hw hx hy hz ia ib ic id ie if ig ih ii ij ik il im in io ip iq ir is it iu iv iw ix iy iz ja jb jc jd je jf jg jh ji jj jk jl jm jn jo jp jq jr js jt ju jv jw jx jy jz ka kb kc kd ke kf kg kh ki kj kk kl km kn ko kp kq kr ks kt ku kv kw kx ky kz la lb lc ld le lf lg lh li lj lk ll lm ln lo lp lq lr ls lt lu lv lw lx ly lz ma mb mc md me mf mg mh mi mj mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt nu nv nw nx ny nz oa ob oc od oe of og oh oi oj ok ol om on oo op oq or os ot ou ov ow ox oy oz pa pb pc pd pe pf pg ph pi pj pk pl pm pn po pp pq pr ps pt pu pv pw px py pz qa qb qc qd qe qf qg qh qi qj qk ql qm qn qo qp qq qr qs qt qu qv qw qx qy qz ra rb rc rd re rf rg rh ri rj rk rl rm rn ro rp rq rr rs rt ru rv rw rx ry rz sa sb sc sd se sf sg sh si sj sk sl sm sn so sp sq sr ss st su sv sw sx sy sz ta tb tc td te tf tg th ti tj tk tl tm tn to tp tq tr ts tt tu tv tw tx ty tz ua ub uc ud ue uf ug uh ui uj uk ul um un uo up uq ur us ut uu uv uw ux uy uz va vb vc vd ve vf vg vh vi vj vk vl vm vn vo vp vq vr vs vt vu vv vw vx vy vz wa wb wc wd we wf wg wh wi wj wk wl wm wn wo wp wq wr ws wt wu wv ww wx wy wz xa xb xc xd xe xf xg xh xi xj xk xl xm xn xo xp xq xr xs xt xu xv xw xx xy xz ya yb yc yd ye yf yg yh yi yj yk yl ym yn yo yp yq yr ys yt yu yv yw yx yy yz
What happens is that incrementing 'z' results in 'aa' and so incrementing a string leads to a somehow convenient chain of ordered char combinations. So by having this knowledge we discover we are failing in our loop condition, since because of how strings are compared it turns out that 'aa' <= 'z' and the loop continues until the value 'za', which in fact is not less or equal than 'z'.
To fix our loop we have to do weird things such as:
for ($ch = 'a'; $ch != 'aa'; $ch++) {
// do something with $ch
}
or
for ($ch = 'a'; strlen($ch) == 1; $ch++) {
// do something with $ch
}
We proved that weird behaviour with the following tests:
public function test_onStringIncrements(
) {
$aChar = "y";
$aChar++;
$this->assertEquals("z", $aChar);
$aChar++;
$this->assertEquals("aa", $aChar);
}
public function test_onCharsIncrements(
) {
$aChar = 'y';
$aChar++;
$this->assertEquals('z', $aChar);
$aChar++;
$this->assertEquals('aa', $aChar);
}
The following code was intended to generate random tokens of length 40 chars:
public static function nextRandomToken(
) {
$valid_chars = array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'%', '-', '.', ',', '\'', '/', '$', '·', '@', '='
);
$result = "";
for ($p = 0; $p < 40; $p++) {
$result = $result . $valid_chars[rand(0, count($valid_chars) - 1)];
}
return $result;
}
The method is fairly simple but it turns out that it randomly breaks a test that checks generated tokens have length 40. Why? Because the literal '·' hasn't length 1 but 2!
Of course this is an encoding issue we don't realize when writing a literal in our code. The obvious behaviour would be that if your write a literal containing N chars its length was N. But as shown this is not true if you write a literal containing some special chars, like '·'.
We proved this behaviour with the following test:
public function test_someCharsArentReallySingleChars(
) {
$this->assertEquals(2, strlen('·'));
$this->assertEquals(2, strlen('¬'));
$this->assertEquals(2, strlen('ñ'));
$this->assertEquals(2, strlen('Ñ'));
$this->assertEquals(2, strlen('ç'));
$this->assertEquals(2, strlen('º'));
$this->assertEquals(2, strlen('ª'));
$this->assertEquals(3, strlen('€'));
}