国内 PHP Composer 镜像列表

Composer 是什么?

Composer 是一个 PHP 包管理的系统,现在越来越多的 PHP 使用 Composer 来管理包。比如 FastAdmin、 ThinkPHP、Laravel 等都是用 Composer 进行 php 包的管理。

镜像列表

国内也很多开发者使用 Composer,但由于不可控因素,官方的服务器常常连接不上。所以这里收集了一下国内镜像列表。(先后次序会不定期调整)

镜像名地址赞助商更新频率备注
阿里云 Composer 镜像https://mirrors.aliyun.com/composer/阿里云96 秒推荐
腾讯云 Composer 镜像https://mirrors.cloud.tencent.com/composer/腾讯云24 小时
PHP 国内 Composer 镜像https://packagist.phpcomposer.com仁润股份24 小时不稳定
华为云 Composer 镜像https://repo.huaweicloud.com/repository/php/华为云未知未知
php.cnpkg.org Composer 镜像https://php.cnpkg.org安畅网络60 秒

配置镜像

全局配置镜像,以下为阿里云镜像配置命令,其它镜像可以参考以下命令。

composer config -g repos.packagist composer https://mirrors.aliyun.com/composer/

MySQL 四种隔离级别

什么是事务

事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。

事务的结束有两种,当事务中的所以步骤全部成功执行时,事务提交。如果其中一个步骤失败,将发生回滚操作,撤消撤消之前到事务开始时的所以操作。

事务的 ACID

事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。这四个特性简称为 ACID 特性。

  • 原子性。事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
  • 一致性。事 务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。
  • 隔离性。一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持续性。也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

Mysql的四种隔离级别

SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

Read Uncommitted(读取未提交内容)

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

Read Committed(读取提交内容)

这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

Repeatable Read(可重读)

这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

Serializable(可串行化)

这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:

  • 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
  • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就有几列数据是未查询出来的,如果此时插入和另外一个事务插入的数据,就会报错。

在MySQL中,实现了这四种隔离级别,分别有可能产生问题如下所示:

Laravel 核心概念讲解

自动依赖注入

什么是依赖注入,用大白话将通过类型提示的方式向函数传递参数。

实例 1

首先,定义一个类:

/routes/web.php
class Bar {}

假如我们在其他地方要使用到 Bar 提供的功能(服务),怎么办,直接传入参数即可:

/routes/web.php
Route::get('bar', function(Bar $bar) {
    dd($bar);
});

访问 /bar,显示 $bar 的实例:

Bar {#272}

也就是说,我们不需要先对其进行实例!如果学过 PHP 的面向对象,都知道,正常做法是这样:

class Bar {}
$bar = new Bar();
dd($bar);

实例 2

可以看一个稍微复杂的例子:

class Baz {}
class Bar 
{
    public $baz;

    public function __construct(Baz $baz)
    {
        $this->baz = $baz;
    }
    
}
$baz = new Baz();
$bar = new Bar($baz);
dd($bar);

为了在 Bar 中能够使用 Baz 的功能,我们需要实例化一个 Baz,然后在实例化 Bar 的时候传入 Baz 实例。

在 Laravel 中,不仅仅可以自动注入 Bar,也可以自动注入 Baz:

/routes/web.php
class Baz {}
class Bar 
{
    public $baz;

    public function __construct(Baz $baz)
    {
        $this->baz = $baz;
    }
    
}

Route::get('bar', function(Bar $bar) {
       dd($bar->baz);
});

显示结果:

Baz {#276}

小结

通过上述两个例子,可以看出,在 Laravel 中,我们要在类或者函数中使用其他类体用的服务,只需要通过类型提示的方式传递参数,而 Laravel 会自动帮我们去寻找响对应的依赖。

那么,Laravel 是如何完成这项工作的呢?答案就是通过服务容器。

服务容器

什么是服务容器

服务容器,很好理解,就是装着各种服务实例的特殊类。可以通过「去餐馆吃饭」来进行类比:

  • 吃饭 – 使用服务,即调用该服务的地方
  • 饭 – 服务
  • 盘子 – 装饭的容器,即服务容器
  • 服务员 – 服务提供者,负责装饭、上饭

这个过程在 Laravel 中如何实现呢?

定义 Rice 类:

/app/Rice.php
<?php

namespace App;

class Rice
{
    public function food()
    {
        return '香喷喷的白米饭';
    }
}
  • 把饭装盘子

在容器中定义了名为 rice 的变量(你也可以起其他名字,比如 rice_container),绑定了 Food 的实例:

app()->bind('rice', function (){
    return new \App\Rice();
});

也可以写成:

app()->bind('rice',\App\Rice::class);

现在,吃饭了,通过 make 方法提供吃饭的服务:

Route::get('eat', function() {
       
       return app()->make('rice')->food(); 
       // 或者 return resolve('rice')->food();

});

make 方法传入我们刚才定义的变量名即可调用该服务。

访问 /eat,返回 香喷喷的白米饭

为了方便起见,我们在路由文件中直接实现了该过程,相当于自给自足。但是服务通常由服务提供者来管理的。

因此,我们可以让 AppServiceProvider 这个服务员来管理该服务:

/app/Providers/AppServiceProvider.php
namespace App\Providers;

public function register()
{
    $this->app->bind('food_container',Rice::class);
}

更为常见的是,我们自己创建一个服务员:

$ php artisan make:provider RiceServiceProvider

注册:

/app/Providers/RiceServiceProvider.php
<?php

use App\Rice;
public function register()
{
    $this->app->bind('rice',Rice::class);
}

这里定义了 register() 方法,但是还需要调用该方法才能真正绑定服务到容器,因此,需要将其添加到 providers 数组中:

/config/app.php
'providers' => [
   App\Providers\RiceServiceProvider::class,
],

这一步有何作用呢?Laravel 在启动的时候会访问该文件,然后调用里面的所有服务提供者的 register() 方法,这样我们的服务就被绑定到容器中了。

小结

通过上述的例子,基本上可以理解服务容器和服务提供者的使用。当然了,我们更为常见的还是使用类型提示来传递参数:

use App\Rice;

Route::get('eat', function(Rice $rice) {
       return $rice->food();

});

在本例中,使用自动依赖注入即可。不需要在用 bind 来手动绑定以及 make 来调用服务。那么,为什么还需要 bind 和 make 呢? make 比较好理解,我们有一些场合 Laravel 不能提供自动解析,那么这时候手动使用 make 解析就可以了,而 bind 的学问就稍微大了点,后面将会详细说明。

门面

门面是什么,我们回到刚才的「吃饭」的例子:

Route::get('eat', function(Rice $rice) {
       return $rice->food();

});

在 Laravel,通常还可以这么写:

Route::get('eat', function() {
       return Rice::food();
});

或者

Route::get('eat', function() {
       return rice()->food();
});

那么,Laravel 是如何实现的呢?答案是通过门面。

门面方法实现

先来实现 Rice::food(),只需要一步:

/app/RiceFacade.php
<?php 

namespace App;
use Illuminate\Support\Facades\Facade;

class RiceFacade extends Facade
{
   
    protected static function getFacadeAccessor()
    {
        return 'rice';
    }
}

现在,RiceFacade 就代理了 Rice 类了,这就是门面的本质了。我们就可以直接使用:

Route::get('eat', function() {

    dd(\App\RiceFacade::food());

});

因为 \App\RiceFacade 比较冗长,我们可以用 php 提供的 class_alias 方法起个别名吧:

/app/Providers/RiceServiceProvider.php
public function register()
{  
   $this->app->bind('rice',\App\Rice::class);
   class_alias(\App\RiceFacade::class, 'Rice');
}

这样做的话,就实现了一开始的用法:

Route::get('eat', function() {
       return Rice::food();
});

看上去就好像直接调用了 Rice 类,实际上,调用的是 RiceFacade 类来代理,因此,个人觉得Facade 翻译成假象比较合适。

最后,为了便于给代理类命名,Laravel 提供了统一命名别名的地方:

/config/app.php

'aliases' => [

    'Rice' => \App\RiceFacade::class,

],

门面实现过程分析

首先:

Rice::food();

因为 Rice 是别名,所以实际上执行的是:

\App\RiceFacade::food()

但是我们的 RiceFacade 类里面并没有定义静态方法 food 啊?怎么办呢?直接抛出异常吗?不是,在 PHP 里,如果访问了不可访问的静态方法,会先调用 __callstatic,所以执行的是:

\App\RiceFacade::__callStatic()

虽然我们在 RiceFacade 中没有定义,但是它的父类 Facade 已经定义好了:

/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php
public static function __callStatic($method, $args)
{   
     
     // 实例化  Rice {#270}
    $instance = static::getFacadeRoot();
    
     // 实例化失败,抛出异常
    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }
    
     // 调用该实例的方法
    return $instance->$method(...$args);
}

主要工作就是第一步实例化:

public static function getFacadeRoot()
{
    return static::resolveFacadeInstance(static::getFacadeAccessor());
    // 本例中:static::resolveFacadeInstance('rice')
}

进一步查看 resolveFacadeInstance() 方法:

 protected static function resolveFacadeInstance($name)
    {   
          // rice 是字符串,因此跳过该步骤
        if (is_object($name)) {
            return $name;
        }
         
         // 是否设置了 `rice` 实例
        if (isset(static::$resolvedInstance[$name])) {
            return static::$resolvedInstance[$name];
        }
         
        return static::$resolvedInstance[$name] = static::$app[$name];
    }

第一步比较好理解,如果我们之前在 RiceFacade 这样写:

protected static function getFacadeAccessor()
{

    return new \App\Rice;

}

那么就直接返回 Rice 实例了,这也是一种实现方式。

主要难点在于最后这行:

return static::$resolvedInstance[$name] = static::$app[$name];

看上去像是在访问 $app数组,实际上是使用 数组方式来访问对象,PHP 提供了这种访问方式接口,而 Laravel 实现了该接口。

也就是说,$app 属性其实就是对 Laravel 容器的引用,因此这里实际上就是访问容器上名为 rice 的对象。而我们之前学习容器的时候,已经将 rice 绑定了 Rice 类:

public function register()
{  
   $this->app->bind('rice',\App\Rice::class);
   // class_alias(\App\RiceFacade::class, 'Rice');
}

所以,其实就是返回该类的实例了。懂得了服务容器和服务提供者,理解门面也就不难了。

辅助方法实现

辅助方法的实现,更简单了。不就是把 app->make('rice') 封装起来嘛:

/vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
if (! function_exists('rice')) {
  
    function rice()
    {   
        return app()->make('rice');
        // 等价于 return app('rice');
        // 等价于 return app()['rice'];
    }
} 

然后我们就可以使用了:

Route::get('eat', function() {

    dd(rice()->food());
});

小结

Laravel 提供的三种访问类的方式:

  • 依赖注入:通过类型提示的方式实现自动依赖注入
  • 门面:通过代理来访问类
  • 辅助方法:通过方法的方式来访问类

本质上,这三种方式都是借助于服务容器和服务提供者来实现。那么,服务容器本身有什么好处呢?我们接下来着重介绍下。

IOC

不好的实现

我们来看另外一个例子(为了方便测试,该例子都写在路由文件中),假设有三种类型的插座:USB、双孔、三孔插座,分别提供插入充电的服务:

class UsbsocketService
{
    public function insert($deviceName){
        return $deviceName." 正在插入 USB 充电";
    }
}

class DoubleSocketService
{
    public function insert($deviceName){
        return $deviceName." 正在插入双孔插座充电";
    }
}

class ThreeSocketService
{
    public function insert($deviceName){
        return $deviceName." 正在插入三孔插座充电";
    }
}

设备要使用插座的服务来充电:

class Device {

    protected $socketType; // 插座类型
    public function __construct()
    {
        $this->socketType = new UsbSocketService();
    }

    public function power($deviceName)
    {    
        return $this->socketType->insert($deviceName);
    }
}

现在有一台手机要进行充电:

Route::get('/charge',function(){
       
   $device = new Device();
   return $device->power("手机");
    
});

因为 Laravel 提供了自动依赖注入功能,因此可以写成:

Route::get('/charge/{device}',function(Device $device){
       
   return $device->power("手机");
    
});

访问 /charge/phone,页面显示 phone 正在插入 USB 充电

假如,现在有一台电脑要充电,用的是三孔插座,那么我们就需要去修改 Device 类:

$this->socketType = new ThreeSocketService();

这真是糟糕的设计,设备类对插座服务类产生了依赖。更换设备类型时,经常就要去修改类的内部结构。

好的实现

为了解决上面的问题,可以参考「IOC」思路:即将依赖转移到外部。来看看具体怎么做。

首先定义插座类型接口:

interface SocketType {
    public function insert($deviceName);
}

让每一种插座都实现该接口:

class UsbsocketService implements SocketType
{
    public function insert($deviceName){
        return $deviceName." 正在插入 USB 充电";
    }
}

class DoubleSocketService implements SocketType
{
    public function insert($deviceName){
        return $deviceName." 正在插入双孔插座充电";
    }
}

class ThreeSocketService implements SocketType
{
    public function insert($deviceName){
        return $deviceName." 正在插入三孔插座充电";
    }
}

最后,设备中传入接口类型而非具体的类:

class Device {

    protected $socketType; // 插座类型
    public function __construct(SocketType $socketType) // 传入接口
    {
        $this->socketType = $socketType;
    }

    public function power($deviceName)
    {    
        return $this->socketType->insert($deviceName);
    }
}

实例化的时候再决定使用哪种插座类型,这样依赖就转移到了外部:

Route::get('/charge',function(){
   
   $socketType = new ThreeSocketService();
   $device = new Device($socketType);
   echo $device->power("电脑");    
});

我们现在可以再不修改类结构的情况下,方便的更换插座来满足不同设备的充电需求:

Route::get('/charge',function(){
   
   $socketType = new DoubleSocketService();
   $device = new Device($socketType);
   echo $device->power("台灯");    
});

自动依赖注入的失效

上面举的例子,我们通过 Laravel 的自动依赖注入可以进一步简化:

Route::get('/charge',function(Device $device){ 
       echo $device->power("电脑");
});

这里的类型提示有两个,一个是 Device $device,一个是 Device 类内部构造函数传入的 SocketType $sockType。第一个没有问题,之前也试过。但是第二个 SocketType 是接口,而 Laravel 会将其当成类试图去匹配 SocketType 的类并将其实例化,因此访问 /charge 时候就会报错:

Target [SocketType] is not instantiable while building [Device].

错误原因很明显,Laravel 没法自动绑定接口。因此,我们就需要之前的 bind 方法来手动绑定接口啦:

app()->bind('SocketType',ThreeSocketService::class);
Route::get('/charge',function(Device $device){
       
       echo $device->power("电脑");
    
});

现在,如果要更换设备,我们只需要改变绑定的值就可以了:

app()->bind('SocketType',DoubleSocketService::class);
Route::get('/charge',function(Device $device){
       
       echo $device->power("台灯");
    
});

也就是说,我们将依赖转移到了外部之后,进一步由第三方容器来管理,这就是 IOC。

契约

契约,不是什么新奇的概念。其实就是上一个例子中,我们定义的接口:

interface SocketType {
    public function insert($deviceName);
}

通过契约,我们就可以保持松耦合了:

public function __construct(SocketType $socketType) // 传入接口而非具体的插座类型
{
    $this->socketType = $socketType;
}

然后服务容器再根据需要去绑定哪种服务即可:

app()->bind('SocketType',UsbSocketService::class);
app()->bind('SocketType',DoubleSocketService::class);
app()->bind('SocketType',ThreeSocketService::class);

参考资料: