首页 PHP 正文
227

PHP中一些容易忽视的小问题集锦(持续更新中...)

  • yiqingpeng
  • 2015-08-31
  • 0
  •  
1、关于定义函数的问题
echo func(); //func was called
exit;  //忽略我的存在了吗
function func(){
     return 'func was called';
}
上面这个例子中,exit并没有影响到它后面的函数定义代码。也就是说函数定义在代码任何位置都是可以的。但是如果在exit语句后面使用include或者require进行文件包含的方式进行函数定义是无效的。而且不管是include还是require,如果要使用被包含文件里面的类或者函数的话,一定要在使用之前进行包含动作,顺序不能反。

2、字符串比较时的隐患
var_dump( md5('240610708') == md5('QNKCDZO') );//bool(true);
var_dump( md5('aabg7XSs') == md5('aabC9RqS')); //bool(true);
var_dump( sha1('aaroZmOk') == sha1('aaK1STfY')); //bool(true);
var_dump( sha1('aaO8zKZF') == sha1('aa3OFF9m')); //bool(true);
var_dump( '0010e2' == '1e3'); //bool(true);
var_dump( '0x1234Ab' == '1193131'); //bool(true);
var_dump( '0xABCdef' == '      0xABCdef'); //bool(true);
我们都知道等于与全等于的区别,等于(==)运算符会自动进行类型转换,比如 '0x1234Ab'这个字符串用在等于(==)运算当中,系统会认为它是一个十六进制表示的数值, 所以它等于十进制的1193131。像一些加密函数,比如上面例子中的md5, sha1这些加密之后的字符串有可能出现0x之类的进制前导符号,导致进行比较时不能按字符串原样比较,所以只要把上面的例子中的等于(==)运算符改成全等于(===)就不会有问题了。所说PHP7将不再视0x开头的字符串为十六进制了。

3、调用远程接口时注意的一点细节
$ret = curl_excute($ch);
$ret = json_decode($ret);
if($ret->code==0){ //远程接口约定code为0时表示调用成功。
    //进行成功操作
}else{
   //进行失败操作
}
很多接口不知道为什么定义状态码code等于0时表示成功,对于弱类型语言而言,null==0也是成立的。如果接口正常返回,那上面代码会没有问题,但是如果出现网络问题导致接口返回的是null,那么code==0也会成立(你也不能用全等于啊,谁知道接口返回的0是字符串类型还是数值类型),这就与事实相悖了。所以保险的做法是在条件判断时,加个isset($ret->code)的并且关系条件。

4、switch语句中用的比较是等于(==),而不是全等于。这点与javascript中的switch语句是不同的。

5、注意组合运算符的计算顺序
$a=0;
$a += (++$a) + (++$a);//$a最终的值是5,不是4.
/*
$a += $b + $c
相当于
$a = ($b + $c) + $a
而不是
$a = $a + ($b + $c)
*/

6、print结构
echo '5'.(print '2')+3; //output 254;
首先print 2,输出 2
因为print会返回1,所以接着运行echo '5'. 1 + 3;//54, 注意1和'.'之间有个空格,否则会语法报错。
两次输出连在一起就是254.

7、json_encode编码对象的注意点
json_encode除了可以对数组进行编码之外,还可以传一个对象,如果是对象的话,对象里面的私有属性、const常量、方法等是会被忽略的。

8、chmod($filename, $mode)方法为什么有时候达不到效果
chmod作用是更改给定文件的权限,但是它有以下几点需要注意:
a)第二个参数必须是八进制,比如0777,而不能是类似777或者“u+rwx,go+rwx"这些。(4-read, 2-write, 1-execute)
b)如果开启了安全模式,那么目标文件的所有者和脚本的执行者必须是同一用户,否则无法修改。

9、字符串也可以这样引用单个字符(类似数组下标):
$str = "abcd";
echo $str[0];//out "a" 或者
echo $str{0};// out "a"
但是字符串常量不行
const MY_STR='abcd';
echo MY_STR[0];//错误的
字符串甚至可以被list()解构,但是到PHP7之后不再有此特性,需要使用str_split().

10、魔术变量__CLASS__要注意的点:
__CLASS__保存是其所在类的类名,它是静态绑定的,只与它出现的地方有关,继承关系并不会影响到它的值,如下代码:
class A{
    function __construct(){
        echo __CLASS__;   //此处__CLASS__出现在类A里面,所以它的值永远是“A",即使在A的子类B里面也是不会变的。
        //echo get_class($this); //如果想得到真正运行时的类名的话,用get_class($this)函数获得
    }
    static function show(){
        echo __CLASS__; //静态方法同样也是如此,如果要得到运行时的类名,用get_called_class(),不过此方法需要php
//的版本在5.3.0以上,低版本需要自行实现,网上有这样的方法。
    }
}

class B extends A{
    function showMe(){
        echo __CLASS__;   //此外因为__CLASS__在类B里面,所以保存的值是B
    }
}
$b = new B();   // output A
$b->showMe();   // output B
B::show();        // output A

11、include与require的区别
include 包含的目标文件不存在时是抛出警告,而require是抛出致命错误并终止程序的执行。除此之外两者的特性都是一样。

12、注意一些常用的安全性措施
a) 敏感目录防止web访问,比如用来存放用户上传身份证的目录应该禁止访问。
b) 使用mysqli的预编绎语句来防止sql注入。
c)  注意防xss,用户输入的文字最好是用htmlspecialchars函数编码一下;利用jq渲染文本时尽量使用$().text()方法而不是$().html()方法,特别是从URL后面取GET参数进去渲染时,更要警惕。尽量避免随意种植cookie.
d)  预防CSRF,每个与后台交互的请求都带上一个由后台生成的token参数,该参数生成时保存在session中,用来对比请求中带来的token是否一致。
e)  严格控制上传文件的类型,上传文件的类型不能使用上传文件数组中的MIME值,而应该查看此文件的后缀名,并且保存文件时进行重命名以防止某些文件名出现解析漏洞问题。
f)  对用户密码进行更高层次的hash加密,目前的md5还是很容易通过碰撞破解出来。
g) 验证码比对完成之后要清理掉,如果不清理,那么用户只要输对第一次,后面都可以重复输同样值进行验证。还有一点,一定要判断用户提交的验证码是不是空值。
h) call_user_func(_array)要小心处理用户输入的参数值。

13、类中的static属性和static方法都是可以直接用类名访问的,同时static方法其实也是可以用实例化对象访问的(ClassName::method() 或者 $object::method()都是可以的),静态属性亦如此。 另外注意的是,static方法中不能出现$this关键字。

14、私有变量真的不能被外部访问到吗?事实可能并不总是如此。同属一个类的实例在方法体中是可以访问私有属性的。
class Foo{
     private $var;
     public function lookInto(Foo $obj){
            $obj->var = 'abc'; //works
            echo $obj->var; //works
    }
}
同样的,protected也是可以用类似的方式进行访问:
class Foo{
    protected $hi = 'hi';
    protected function sayHi(){
        echo $this->hi;
    }
}
class Bar extends Foo{
    public static function callSayHi(Foo $foo){
        $foo->hi = 'Hi, foo'; //access the protected property of $foo
        $foo->sayHi();// output Hi, foo
    }
}
所以属性的可见性是针对于类空间而言,而不是实例。此特性为实现装饰者模式又提供了更多灵活控制的手段,但同时注意到,随意修改protected属性的值也许会给下游调用者造成不可预测的数据异常问题。

15、spl_autoload_register中没必要使用require_once和include_once, 使用require和include就可以了,毕竟没有once的校验在性能上还是有点优势。

16、如何调用grandParent类中的方法?
class GrandPa{
    protected function call(){}
}
class Pa extends GrandParent{
    protected function call(){}
}
class Son extends Pa{
    protected function call(){
        parent::call();// In Pa class
        $grand = get_parent_class(get_parent_class($this));
        $grand::call(); // In GrandPa class
    }
}

17、不依赖clone,通过对象实例也能派生对象.
$a = new SomeClass;
$b = new $a; //$b是一个全新的实例,除了同样来自SomeClass之外,跟$a没有任何关系。

18、Trait虽然不可以实例化,但可以实现静态方法调用:
trait TFoo{
    public static function doIt(){}
}
TFoo::doIt(); // works.

19、Trait的静态属性在调用它的实例中是独立的:
class Base{
    public static $hi = "In Base";
}
class AofBase extends Base{}
class BofBase extends Base{}
AofBase::$hi和BofBase::$hi是同一个静态变量。
如果引入的是trait, 则不然:
trai T{
    public static $hi = "ABC";
}
class A{
    use T;
}
class B{
    use T;
}
B::$hi = '123';
echo A::$hi; //ABC
echo B::$hi; //123

20、匿名类生成的对象具有共同的类空间:
function getAnoObj(){
    return new class{
         public static $p = [];
    };
}
$a = getAnoObj();
$a::$p[] = 'a';
$b = getAnoObj();
$b::$p[] = 'b';
var_dump($b::$p); // array('a', 'b');

21、关于数组函数的一些小技巧:
a) 去除数组中的空值:
$out = array_filter(array_map('trim', ['  a', "", 0, null, "b  "])); // ['a', 'b'] , 注意array_filter会保留键名。
b) foreach中展开数组元素:foreach ([['a','b'], ['1', '2']] as list($a, $b)) {...}
c) 提取二维数组中的一列作为键(如果元素数量达到百万级建议使用foreach手动构造):
$out = array_column([['id' => 1, 'name'=>'Jil', 'age'=>21], ['id' => 2, 'name'=>'Bob', 'age'=>11]], null, 'id');
//array_column第二个参数表示提取哪一列,如果为null,将提取所有列;第三个参数表示用哪一列作为新数组的键。

22、clone可以实现多个对象共享一个属性,并且可以规避掉构造方法的调用。
class MyRef{
    public $ref;
    public function __construct(){
        $this->ref = &$this->ref;  //将属性自身的引用保存到属性中
    }
}
$a = new MyRef;  // 构造方法仅在$a初始化的时候被调用
$a->ref = 1;
$b = clone $a;
$b->ref = 2;
echo $a->ref;  // output: 2

23、forward_static_call 与 call_user_func 虽然都可以用来调用用户函数,但是前者只能用在类方法中,用以实现后期静态绑定的功能,比较如下示例:
class Beer { 
    const NAME = 'Beer'; 
    public static function printed(){ 
        echo static::NAME . PHP_EOL; 
    } 


class Ale extends Beer { 
    const NAME = 'Ale'; 
    public static function printed(){ 
        forward_static_call(array('parent','printed')); // Ale
        call_user_func(array('parent','printed'));  // Ale

        forward_static_call(array('Beer','printed'));  // Ale
        call_user_func(array('Beer','printed')); // Beer
    } 


Ale::printed(); 

24、file_exists的陷阱:如果一个文件开始是存在的,然后被外部动作删除(非unlink()调用, 因为unlink()自带清cache机制 ),因为cache的缘故,file_exists在文件删除之后依然会返回True. 包括is_file(), is_dir()都会受缓存影响。

25、匿名函数的递归调用:
$sum = function ($v) use (&$sum){ //注意一定要用引用传递
    if ($v == 0 ) return $v;
    return $v + $sum($v-1);
};

26、is_callable的特性:
a) 能感知context, 也就是说在类方法内部检测本类的私有方法返回true, 在外部返回false. method_exists对私有方法也返回true.
b) 一个类如果定义了__call方法,那么传入任何不存在的方法名都返回true. 而method_exists返回的是false.
c) 一个类如果定义了__invoke方法,那么检测对象实例的时候会返回true.
d) is_callable会触发__autoload机制,所以性能上会有点瑕疵。

27、foreach中的引用陷阱:
$arr = [
    1,
    2,
    3,
];
foreach ($arr as &$row) {
}
//unset($row);

foreach ($arr as $row){
    echo $row, "<br>";
}
/*output:
1
2
2
*/
记得loop1结束后一定要unset($row)。发生此问题的原因是由于:
a)foreach语句中的变量范围是全局的。
b)$row作为引用变量时,在循环结束后,仍然是一个引用变量,并且指向了$arr的最后一个元素,即 $row==>$arr[2],
后续任何对$row的赋值,都会改变$arr[2]的值。

正在加载评论...