月度归档:2018年04月

Laravel FileSystem详解

存储一个文件,需要知道存放在哪个磁盘以及磁盘上的那个位置,还需要设置文件的可见性(private或public),如果是public的,还需要知道怎么访问。

不管是本地磁盘还是网络磁盘,都可以抽象为磁盘,在磁盘上,可做的操作一样,这个就是League\Flysystem包做的事情。而Laravel的文件系统,是对这个包的二次封装(提供一个新的适配器)。

服务提供:
Illuminate\Filesystem\FilesystemServiceProvider

class FilesystemServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->registerNativeFilesystem();

        $this->registerFlysystem();
    }

    protected function registerNativeFilesystem()
    {
        $this->app->singleton('files', function () {
            return new Filesystem;
        });
    }

    protected function registerFlysystem()
    {
        $this->registerManager();

        $this->app->singleton('filesystem.disk', function () {
            return $this->app['filesystem']->disk($this->getDefaultDriver());
        });

        $this->app->singleton('filesystem.cloud', function () {
            return $this->app['filesystem']->disk($this->getCloudDriver());
        });
    }

    protected function registerManager()
    {
        $this->app->singleton('filesystem', function () {
            return new FilesystemManager($this->app);
        });
    }
}

首先注意到这里的registerNativeFilesystem直接是往容器中注册files,指向Illuminate\Filesystem\Filesystem实例。它是Laravel提供的一个本地文件系统封装,只是用来方便操作本地任意的文件或目录。这个对象内部使用了Symfony\Component\Finder\Finder来实现文件的查找操作,同时提供了一致的API,比如get()、put()、exists()、delete()、prepend()、append()等等,特别:

// 先上锁
    public function sharedGet($path)
    {
        $contents = '';

        $handle = fopen($path, 'rb');

        if ($handle) {
            try {
                if (flock($handle, LOCK_SH)) {
                    clearstatcache(true, $path);

                    $contents = fread($handle, $this->size($path) ?: 1);

                    flock($handle, LOCK_UN);
                }
            } finally {
                fclose($handle);
            }
        }

        return $contents;
    }

// 创建文件或目录的符号链接(Windows下如何实现)
    public function link($target, $link)
    {
        if (! windows_os()) {
            return symlink($target, $link);
        }

        $mode = $this->isDirectory($target) ? 'J' : 'H';

        exec("mklink /{$mode} \"{$link}\" \"{$target}\"");
    }

注意,可以通过app(‘files’)获取这个对象,另外它对应了一个叫File的Facade,可以充分使用:

$path = public_path();
$file = $path . '/t.txt';
if (\File::exists($file)) {
    \File::delete($file);
}

回到服务提供者,除了registerNativeFilesystem,还有registerManager,实际是一个文件系统管理器,文件系统可以看做是一个磁盘,可以有很多磁盘(比如本地磁盘,网络磁盘),那就需要一个管理器管理这些磁盘,也方便切换。这里的磁盘又可分为默认磁盘和默认网络磁盘(这两个不冲突),默认磁盘和默认网络磁盘可以一样。对应的配置文件:

return [
    'default' => env('FILESYSTEM_DRIVER', 'local'),

    'cloud' => env('FILESYSTEM_CLOUD', 's3'),

    'disks' => [

         'local' => [
             'driver' => 'local',
             'root' => storage_path('app'),
         ],

        'public' => [
            'driver' => 'local',
            'root' => storage_path('app/public'),
            'url' => env('APP_URL').'/storage',
            'visibility' => 'public',
        ],

        's3' => [
            'driver' => 's3',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
        ],

    ],

];

注意disks数组,Key是磁盘名称,原则上可以随意命名,切换磁盘时用的就是这个名称。里面的driver对于内置的来说,就是固定的,如果是自定义的驱动,对应的就是自定义的名字(需要按照League\Flysystem套路来实现驱动),其它配置是因驱动不同而不同。

这里需要看明白的是local和public磁盘的差别,public磁盘存储路径在/storage/app/public中,可见性是“public”的,访问链接是/public/storage,这里意味着需要把/storage/app/public和/public/storage建立一个符号链接,可以调用Illuminate\Filesystem\Filesystem的link()方法可以完成,Laravel本身提供了一个专门针对这个情况的命令行工具:

php artisan storage:link

// Illuminate\Foundation\Console\StorageLinkCommand
// 如下代码
        $this->laravel->make('files')->link(
            storage_path('app/public'), public_path('storage')
        );

默认存储到本地磁盘中的文件(位置:/storage/app/)不是公开的,但是/storage/app/public是公开的,所有需要做一个符号链接。

如果需要存放到一个公开的文件,那么需要切换到public磁盘,然后保存即可:

$public = \Storage::disk("public");
$public->put('folder', 'file.txt');

Storage对应的就是磁盘管理器(app(“filesystem”))。在Illuminate\Filesystem\FilesystemManager中,disk就是一个Illuminate\Filesystem\FilesystemAdapter的实例,它接收一个League\Flysystem\FilesystemInterface类型的实例(League\Flysystem\Filesystem是实现类),而这个实例又需要一个League\Flysystem\AdapterInterface的实例,配置会传递到这个适配器。最终结果就是Laravel通过一个自己的适配器,把League\Flysystem\FilesystemInterface接口的方法更换了一套API名称(当然还有其它的集成)。

1 自定义文件系统
Laravel的适配器需要一个League\Flysystem\Filesystem实例,而League\Flysystem\Filesystem需要一个League\Flysystem\AdapterInterface适配器实例,所以套路是:

class XxxAdaper implements \League\Flysystem\AdapterInterface
{}

Storage::extend('xxx', function ($app, $config) {
            return new Filesystem(new League\Flysystem\Filesystem(new XxxAdaper($config)));
        });

Storage::extend()第一个参数是驱动名称(不是磁盘名称),然后这个磁盘就可用,这里需要做的就是实现XxxAdaper适配器。

2 启用和配置S3
对于S3, Laravel提供了内置支持,不过还需要引入S3的SDK才能正常工作。

AWS提供的SDK,底层服务使用GuzzleHttp来通信,所以理论上应该提供了针对它的相关的控制选项。 SDK中的Aws\S3\S3Client继承自Aws\AwsClient,查看其构造函数说明:

__construct ( array $args ) 
 
http: (array, default=array(0)) Set to an array of SDK request options to apply to each request (e.g., proxy, verify, etc.).

针对Http控制的仅仅有这一样的说明。也没有点出默认是使用GuzzleHttp。实际上,所有针对GuzzleHttp可用的配置参数,这里都可以传递进来:

'http' => [
                'connect_timeout' => 20,
                'timeout' => 60,
                'verify' => false
            ],

比如这里的超时控制,GuzzleHttp默认是0,意思就不超时,这个在后台进程中,经常因为这个默认值,导致进程僵死。又比如,如果要控制其走代理:

// 控制S3是否使用代理
$http_proxy = env('S3_PROXY', false);
if(!empty($http_proxy)) {
    $proxy = env($http_proxy, false);
    if(!empty($proxy)) {
        $config['disks']['s3']['http']['proxy'] = [
            'http'  => 'tcp://'.$proxy,
            'https' => 'tcp://'.$proxy
        ];
    }
}

2 文件下载
磁盘重新中提供了一个download()方法,可以非常方便地下载文件:

return \Storage::download('files/zVolRIUofhOdx9BDJP04WMGoJJonmLcZfITcXI2s.docx');

3 文件上传
Illuminate\Http\Request使用了Illuminate\Http\Concerns\InteractsWithInput,这个trait包含了与输入相关的方法:

// 上传一个文件
<input type="file" name="file" />

// 上传多个文件
<input type="file" name="file[]" />
<input type="file" name="file[]" />

$request->hasFile("file");
$request->file("file");

服务端,控制前中都可以使用$request->hasFile(“file”)判断是否存在文件,然后通过$request->file(“file”)把文件取回。

public function hasFile($key)
    {
        if (! is_array($files = $this->file($key))) {
            $files = [$files];
        }

        foreach ($files as $file) {
            if ($this->isValidFile($file)) {
                return true;
            }
        }

        return false;
    }

protected function isValidFile($file)
    {
        return $file instanceof SplFileInfo && $file->getPath() !== '';
    }

public function file($key = null, $default = null)
    {
        return data_get($this->allFiles(), $key, $default);
    }

   public function allFiles()
    {
        $files = $this->files->all();

        return $this->convertedFiles
                    ? $this->convertedFiles
                    : $this->convertedFiles = $this->convertUploadedFiles($files);
    }

    protected function convertUploadedFiles(array $files)
    {
        return array_map(function ($file) {
            if (is_null($file) || (is_array($file) && empty(array_filter($file)))) {
                return $file;
            }

            return is_array($file)
                        ? $this->convertUploadedFiles($file)
                        : UploadedFile::createFromBase($file);
        }, $files);
    }

注意,$this->files是所有上传的文件(集合),每个文件都会转换为Illuminate\Http\UploadedFile对象。

PHP中,对文件有三个标准类:

SplFileInfo
SplFileObject
SplTempFileObject

SplTempFileObject继承SplFileObject,SplFileObject继承SplFileInfo,SplFileInfo实现了一个文件的封装,提供了非常常用的方法。

Symfony\Component\HttpFoundation\File\File继承了SplFileInfo,在它之上,提供了guessExtension()、getMimeType()方法。

Symfony\Component\HttpFoundation\File\UploadedFile继承了Symfony\Component\HttpFoundation\File\File,在它基础之上提供了文件上传需要的特定方法,比如:getClientOriginalName()、getClientOriginalExtension()、getClientMimeType()、guessClientExtension()、isValid()、move()等方法,

Illuminate\Http\UploadedFile继承了Symfony\Component\HttpFoundation\File\UploadedFile,提供了与框架相关的保存文件的方法。比如:store()、storePublicly()、storeAs()等。

例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/test/upload" method="post" enctype="multipart/form-data">
    {{ csrf_field() }}
    {{-- 单选 --}}
    <input type="file" name="file" />               
    {{-- 多选,表单名字不带[]--}}
    <input type="file" name="files" multiple />
     {{-- 多选,表单名字带[]--}}
    <input type="file" name="files1[]" multiple />
    {{-- 多选加单选 --}}
    <input type="file" name="files2[]" multiple />
    <input type="file" name="files2[]" />
    
    <br />
    <input type="submit" value="提交" />
</form>
</body>
</html>

// 文件处理
   public function upload(Request $request)
   {
      $files = $request->allFiles();
      print_r($files);
    }

输出:

Array
(
    [file] => Illuminate\Http\UploadedFile Object
        (
            [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
            [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => a.jpg
            [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/jpeg
            [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 3305
            [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
            [hashName:protected] => 
            [pathName:SplFileInfo:private] => /private/var/tmp/php07Byk0
            [fileName:SplFileInfo:private] => php07Byk0
        )

    [files] => Illuminate\Http\UploadedFile Object
        (
            [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
            [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => test.png
            [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/png
            [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 17417
            [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
            [hashName:protected] => 
            [pathName:SplFileInfo:private] => /private/var/tmp/phpd9uAtT
            [fileName:SplFileInfo:private] => phpd9uAtT
        )

    [files1] => Array
        (
            [0] => Illuminate\Http\UploadedFile Object
                (
                    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
                    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => a.jpg
                    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/jpeg
                    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 3305
                    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
                    [hashName:protected] => 
                    [pathName:SplFileInfo:private] => /private/var/tmp/phpzJvo3C
                    [fileName:SplFileInfo:private] => phpzJvo3C
                )

            [1] => Illuminate\Http\UploadedFile Object
                (
                    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
                    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => test.png
                    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/png
                    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 17417
                    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
                    [hashName:protected] => 
                    [pathName:SplFileInfo:private] => /private/var/tmp/phpHCVvIK
                    [fileName:SplFileInfo:private] => phpHCVvIK
                )

        )

    [files2] => Array
        (
            [0] => Illuminate\Http\UploadedFile Object
                (
                    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
                    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => a.jpg
                    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/jpeg
                    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 3305
                    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
                    [hashName:protected] => 
                    [pathName:SplFileInfo:private] => /private/var/tmp/php50xae1
                    [fileName:SplFileInfo:private] => php50xae1
                )

            [1] => Illuminate\Http\UploadedFile Object
                (
                    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
                    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => test.png
                    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/png
                    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 17417
                    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
                    [hashName:protected] => 
                    [pathName:SplFileInfo:private] => /private/var/tmp/phpuHaPFq
                    [fileName:SplFileInfo:private] => phpuHaPFq
                )

            [2] => Illuminate\Http\UploadedFile Object
                (
                    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
                    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => a.jpg
                    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/jpeg
                    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 3305
                    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
                    [hashName:protected] => 
                    [pathName:SplFileInfo:private] => /private/var/tmp/phpXU4PAy
                    [fileName:SplFileInfo:private] => phpXU4PAy
                )

        )
)

这个结果非常清晰:
1 对于单选,识别为一个文件对象
2 对于多选,如果表单name不带[],还是识别为一个文件对象,这个文件对象为最后选中的文件
3 对于多选,表单name必须携带[],这样可以识别为一个数组
4 可以混合使用单选和多选,只要表单name相同即可
5 识别的Key不带[]

文件上传到临时目录后,如果正确上传,会通过验证:

    public function isValid()
    {
        $isOk = UPLOAD_ERR_OK === $this->error;

        return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
    }

通过验证后需要保存到一个正常的目录,可以使用move()方法,不过Laravel提供了一套方案,可以方便地保存到磁盘:

public function store($path, $options = [])
    {
        return $this->storeAs($path, $this->hashName(), $this->parseOptions($options));
    }

直接调用store可以保存文件到默认磁盘,如果需要切换磁盘,可以定义$options,比如要保存到s3,可以设置$options = [‘disk’ => ‘s3’],背后的逻辑实际就是:

\Storage::disk("s3")->putFile($path, new UploadFile());

如果要设置可见性,可以调用storePublicly(),实际调用storeAs(), 修正$options而已。所以:

storePublicly($path)
//等同
store($path, $option = ["visibility" => "public"]);

对于本地驱动,visibility是一个模拟,理应保存到/storage/app/public中,实际上和store并没有差异。如果要保存到public中还是要[‘disk’ => ‘pubic’]才行。

Laravel Socialite 详解

Laravel Socialite提供OAuth认证,目前支持的认证驱动包括Facebook、Twitter、Google、LinkedIn、GitHub 和 Bitbucket。注:其它平台可以到https://socialiteproviders.github.io/中找到。

composer require laravel/socialite

vi config/services.php
'github' => [
    'client_id' => env('GITHUB_CLIENT_ID'),         // Your GitHub Client ID
    'client_secret' => env('GITHUB_CLIENT_SECRET'), // Your GitHub Client Secret
    'redirect' => 'http://your-callback-url',
],

vi .env
GITHUB_CLIENT_ID=4a9750c26b0b62000748
GITHUB_CLIENT_SECRET=0b9bfa15f6d0cdc50000082bd364631cd973c000

注:Key必须为facebook、twitter、linkedin、google、github 或bitbucket,配置哪些key取决于应用需要的提供者。

这里拿Github来测试,到https://github.com/settings/developers添加OAuth应用,对应填写相关信息(注意回调的填写)。

控制器路由:


<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Laravel\Socialite\Facades\Socialite;

class LoginController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Login Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles authenticating users for the application and
    | redirecting them to your home screen. The controller uses a trait
    | to conveniently provide its functionality to your applications.
    |
    */

    use AuthenticatesUsers;

    /**
     * Where to redirect users after login.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest')->except('logout');
    }

    public function redirectTo($platform)
    {
        return Socialite::driver($platform)->redirect();
    }

    public function handleCallback($platform)
    {
        $user = Socialite::driver($platform)->user();

        dd($user);
    }
}


// 路由

Route::get('login/{platform}', 'Auth\LoginController@redirectTo');
Route::get('login/{platform}/callback', 'Auth\LoginController@handleCallback');

首次登陆,把用户插入用户表,然后用Auth::login()让其变成登录状态。以后再次登录,根据ID定位到用户,然后用Auth::login()让其登录。

在重定向到社交媒体页面,要求登录,确认授权,确认后会把一次性授权码重定向到handleCallback,在这个回调中,Socialite::driver($platform)->user()做了深层封装,实际上它首先根据一次性授权码code换取token(刷新token可能也返回),取到token后,再发起一个正常的API调用(携带token),把用户信息取回来。

如果一次性code已经失效,那么将返回401状态码(认证失败),所以安全性的保证是一次性code有效性。

Laravel 多语言详解

要实现多语言,必定是对文本进行提取,表示为key,然后根据语言寻找到到语言文件,定位key对应的值。

先链接一些用法:

/resources
    /lang
        /en
            messages.php
        /zh-CN
            messages.php

# messages.php文件
<?php

return [
    'welcome' => 'Welcome to our application'
];

默认需要使用什么语言,在config/app.php中配置:

'locale' => 'zh-CN',
'fallback_locale' => 'en',

默认语言是zh-CN,备用是en,备用是指当在默认找不到时,切换为备用的寻找语言文件。可以在运行中修改:

$locale = App::getLocale();

if (App::isLocale('en')) {
    App::setLocale($locale);
}

定义翻译文件,根据语言,在resources/lang下面建立JSON文件:

/resources/lang/zh-CN.json

{
    'hello' => '你好' 
}

使用翻译:

echo __("hello")
echo __('messages.welcome');
#在模板中
{{ __("hello") }}
@lang("hello")

双下滑线函数首先到zh-CN.json中定位,如果没有再到lang/zh-CN中根据一定规则定位,如果无法定位,返回key本身。具体来说,如果messages.welcome这个key在zh-CN.json中有定义,就直接使用,如果没有就到lang/zh-CN中定位message文件,然后从这个文件中定位welcome这个key,如还是无法定位,直接返回messages.welcome。

另外,key对应的值可以有占位符,所以双下滑线函数第二参数可以携带一个关联数组:

'welcome' => 'Welcome, :name',

echo __('messages.welcome', ['name' => 'laravel']);

注意:占位符可以是首字母大写,也可以全大写,对应传入的字符串也会做对应的转换(首字母大写或全大写)

具体实现。在config/app.php中会载入serviceProvider(Illuminate\Translation\TranslationServiceProvider):

<?php

namespace Illuminate\Translation;

use Illuminate\Support\ServiceProvider;

class TranslationServiceProvider extends ServiceProvider
{
    protected $defer = true;

    public function register()
    {
        $this->registerLoader();

        $this->app->singleton('translator', function ($app) {
            $loader = $app['translation.loader'];

            $locale = $app['config']['app.locale'];

            $trans = new Translator($loader, $locale);

            $trans->setFallback($app['config']['app.fallback_locale']);

            return $trans;
        });
    }

    protected function registerLoader()
    {
        $this->app->singleton('translation.loader', function ($app) {
            return new FileLoader($app['files'], $app['path.lang']);
        });
    }

    public function provides()
    {
        return ['translator', 'translation.loader'];
    }
}

首先注意到这个serviceProvider是defer的(在需要时才注册服务)。translation.loader到一个FileLoader实例,它需要把文件系统对象注入,它负责怎么取文件,而去哪里取则是第二参数指定的。 loader只是解决了如何load文件,根据什么条件load,然后怎么取回key,是Translator负责,它需要把loader注入,还要告诉默认什么语言和备用语言(对应代码中的translator)。

首先看一下Loader:

<?php

namespace Illuminate\Translation;

use RuntimeException;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Contracts\Translation\Loader;

class FileLoader implements Loader
{
    public function load($locale, $group, $namespace = null)
    {
        if ($group == '*' && $namespace == '*') {
            return $this->loadJsonPaths($locale);
        }

        if (is_null($namespace) || $namespace == '*') {
            return $this->loadPath($this->path, $locale, $group);
        }

        return $this->loadNamespaced($locale, $group, $namespace);
    }
}

主要就是这个load方法,用法举例:

// 解析resources/lang/zh-CN.json
load('zh-CN', '*', '*');

// 解析resources/lang/zh-CN/下的message.php文件
load('zh-CN', 'message') 
load('zh-CN', 'message', '*') 

// 解析ifeeline命名空间(需要先设置ifeeline => vender/ifeeline/ifeeline/src/lang/)中zh-CN/下的message.php
// 然后用resources/lang/vender/ifeeline/zh-CN/message.php覆盖后返回
load('zh-CN', 'message', 'ifeeline') 

第三种用法看起来有点不好理解。这个主要用来对付一些包,它自己本身处理了多语言,举例:

ifeeline/example => vender/ifeeline/example/src 

包名称和命名空间可一样,也可以不一样(勿混淆)。在这个包装中,通常暴露一个serviceProvider,其中会调用translation.loader的addNamespace方法,告诉loader自己对应的包名对应的语言路径,实际就是:

ifeeline-example => vender/ifeeline/example/src/lang

public function addNamespace($namespace, $hint)
    {
        $this->hints[$namespace] = $hint;
    }

当要取回某个文件时,通过loader.load(‘zh-CN’, ‘message’, ‘ifeeline-example’)取回vender/ifeeline/example/src/lang/zh-CN/message.php文件,然后通过translator再定位key。

所以为了可以扩展(添加没有的语言),和修改key对应的值,translator还会操作loader去resource/lang/vender/ifeeline-example/zh-CN下寻找message文件进行覆盖(原来没有就是新增),所以可以把语言包放入到对应位置就实现了覆盖或添加新语言包。一般包中的serviceProvider会提供publish钩子,可以把语言文件拷贝对对应位置(我们进行对应修改以实现覆盖)。

注意到loader的load()方法还是非常涩的,而且没有定位key,这个就是translator的具体实现。

Illuminate\Translation\Translator继承自Illuminate\Support\NamespacedItemResolver,它的parseKey方法用法:

# 返回[ifeeline-example, message, hello]
parseKey("ifeeline-example::message.hello")

# 返回[ifeeline-example, message, hello.hi]]
parseKey("ifeeline-example::message.hello.hi")

# 返回[ifeeline-example, message, null]
parseKey("ifeeline-example::message")

# 返回[null, message, hello]
parseKey("message.hello")

# 返回[null, message, hello.hi]]
parseKey("message.hello.hi")

# 返回[null, message, null]
parseKey("message")

最终来到Illuminate\Translation\Translator:

<?php

namespace Illuminate\Translation;

use Countable;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Collection;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Contracts\Translation\Loader;
use Illuminate\Support\NamespacedItemResolver;
use Illuminate\Contracts\Translation\Translator as TranslatorContract;

class Translator extends NamespacedItemResolver implements TranslatorContract
{
    public function get($key, array $replace = [], $locale = null, $fallback = true)
    {
        list($namespace, $group, $item) = $this->parseKey($key);

        // Here we will get the locale that should be used for the language line. If one
        // was not passed, we will use the default locales which was given to us when
        // the translator was instantiated. Then, we can load the lines and return.
        $locales = $fallback ? $this->localeArray($locale)
                             : [$locale ?: $this->locale];

        foreach ($locales as $locale) {
            if (! is_null($line = $this->getLine(
                $namespace, $group, $locale, $item, $replace
            ))) {
                break;
            }
        }

        // If the line doesn't exist, we will return back the key which was requested as
        // that will be quick to spot in the UI if language keys are wrong or missing
        // from the application's language files. Otherwise we can return the line.
        if (isset($line)) {
            return $line;
        }

        return $key;
    }

    protected function getLine($namespace, $group, $locale, $item, array $replace)
    {
        $this->load($namespace, $group, $locale);

        $line = Arr::get($this->loaded[$namespace][$group][$locale], $item);

        if (is_string($line)) {
            return $this->makeReplacements($line, $replace);
        } elseif (is_array($line) && count($line) > 0) {
            return $line;
        }
    }

}

首先,如果无法定时,直接返回$key。循环语言(指定语言和备用),所以主语言找不到,会去备用语言中找。注意这里的$this->parseKey($key),如果namespace为null,这个方法把其改为*。

具体实现在getLine()方法: 首先load()文件,三个情况,1 /lang/zh-CN.json 2 /lang/zh-CN/message.php 3 包语言文件(以及覆盖文件)。然后到文件中定位具体的key。比如是a => b.c, 那么a文件中应该有一个b数组,它有一个c元素。当然还包括占位符替换的处理。

翻译器get方法最终调用getLine()方法,而get方法都是先解析key的,意味着”hello word.”这样的key会被解析成[null,hello word,”],很明显,无法定位到,所以这里就要说到双下划线函数了:

if (! function_exists('__')) {
    /**
     * Translate the given message.
     *
     * @param  string  $key
     * @param  array  $replace
     * @param  string  $locale
     * @return string|array|null
     */
    function __($key, $replace = [], $locale = null)
    {
        return app('translator')->getFromJson($key, $replace, $locale);
    }
}

这个双下划线函数是从getFromJson()进入的:

    public function getFromJson($key, array $replace = [], $locale = null)
    {
        $locale = $locale ?: $this->locale;

        // For JSON translations, there is only one file per locale, so we will simply load
        // that file and then we will be ready to check the array for the key. These are
        // only one level deep so we do not need to do any fancy searching through it.
        $this->load('*', '*', $locale);

        $line = $this->loaded['*']['*'][$locale][$key] ?? null;

        // If we can't find a translation for the JSON key, we will attempt to translate it
        // using the typical translation file. This way developers can always just use a
        // helper such as __ instead of having to pick between trans or __ with views.
        if (! isset($line)) {
            $fallback = $this->get($key, $replace, $locale);

            if ($fallback !== $key) {
                return $fallback;
            }
        }

        return $this->makeReplacements($line ?: $key, $replace);
    }

先解析zh-CN.json,没有在调用get()。换个说法就是,双下划线函数总是去loader对应json文件,然后才开始常规定位。如果不需要考虑这个全局的json文件,可以使用trans()方法。

写了那么多,实际使用时,只要把全局的key(翻译文件),写入到json, 提炼出来的key写入到某个文件,使用双下划线函数加上完整的key(message.hello)即可搞定多语言问题。

Magento 2.x 安装

Magento 2.x的安装,官方提供了三个方式:
1 直接下载完整的压缩包
2 通过composer安装,并制定使用官方提供的仓库(需要配置)
3 克隆Github上的仓库,然后通过composer安装

第一和第三种方式,本质上没有多大差别。第二种方式中需要指定一个仓库,这个主要Magento本身的扩展市场生态,如果要使用到官方提供的扩展,第二种方式是唯一选择。

第三种安装方式:(https://github.com/magento/magento2)

#Git 克隆
yum install git
git clone https://github.com/magento/magento2.git
cd magento2
git checkout 2.2.3

composer install

#直接下载压缩包(先选中版本)
cd magento2

composer install

Nginx配置:

upstream fastcgi_backend {
   server  127.0.0.1:9001;
}
server {
   listen 80;
   server_name magento2.dev;
   set $MAGE_ROOT /var/www/magento/magento2;
   include /var/www/magento/magento2/nginx.conf.sample;
}

设计模式

在软件工程中,设计模式(Design Pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。设计模式是描述在各种不同场景下,要怎么解决问题的一种方案。

常用设计模式
1 创建型(对象实例化)
抽象工厂模式(Abstract Factory)****
建造者模式(Builder)****
工厂方法模式(Factory Method)****
多例模式(Multiton)
对象池模式(Pool)
原型模式(Prototype)
简单工厂模式(Simple Factory)****
单例模式(Singleton)****
静态工厂模式(Static Factory) ****

2 结构型(类和对象的组合)
适配器模式(Adapter) ****
桥梁模式(Bridge)
组合模式(Composite)
数据映射模式(Data Mapper)
装饰模式(Decorator)
依赖注入模式(Dependency Injection) ****
门面模式(Facade) ****
流接口模式(Fluent Interface)
代理模式(Proxy) ****
注册模式(Registry)

3 行为型(类的对象间通信)
责任链模式(Chain Of Responsibilities) ****
命令行模式(Command)
迭代器模式(Iterator)
中介者模式(Mediator)
备忘录模式(Memento)
空对象模式(Null Object)
观察者模式(Observer) *****
规格模式(Specification)
状态模式(State)
策略模式(Strategy)
模板方法模式(Template Method)
访问者模式(Visitor)

4 其它
委托模式(Delegation)
服务定位器模式(Service Locator) ****
资源库模式(Repository)

来源:https://github.com/domnikl/DesignPatternsPHP

1.1 简单工厂模式、静态工厂模式、工厂方法模式、抽象工厂模式
简单工厂模式、静态工厂模式类似,都是直接约定使用某个方法生产产品;工厂方法模式是将生成产品的方法进行抽离,放入到一个抽象类中,工厂类继承该类并实现其中的工厂方法,本质上和简单工厂模式、静态工厂模式没有差别,不同是由于对生成产品的方法进行来抽离,方便产生不同类型的工厂;

抽象工厂模式与其它模式有较大不同,它是对工厂的抽象;在简单工厂模式、静态工厂模式、工厂方法模式中,工厂是和产品直接相关的,产品和工厂是不相关的(被抽象了,或者说只跟抽象工厂相关)。具体来说:抽象工厂不过是对不同类型的工厂进行抽象,比如某汽车公司可以生产轿车和汽车,由于复杂性,通常不会在一个工厂中生产,会分到轿车厂和汽车厂,但是轿车厂和汽车厂都具备了生产车的能力,当要生产汽车时,抽象工厂决定由汽车厂生产,用户并不知道汽车是由汽车厂生产的(产品和工厂不相关)。

抽象工厂模式通常用来解决较复杂的产品制造逻辑,绝大部分情况,简单工厂模式(包括静态工厂模式和工厂方法模式)可以很好适用。

Laravel中的Manager大量应用简单工厂模式(具体来说是工厂方法模式),比如Auth组件中,Guard管理:

// Illuminate\Contracts\Auth\Factory
<?php

namespace Illuminate\Contracts\Auth;

interface Factory
{
    /**
     * Get a guard instance by name.
     *
     * @param  string|null  $name
     * @return mixed
     */
    public function guard($name = null);

    /**
     * Set the default guard the factory should serve.
     *
     * @param  string  $name
     * @return void
     */
    public function shouldUse($name);
}


// Illuminate\Contracts\Auth\AuthManager
<?php

namespace Illuminate\Auth;

use Closure;
use InvalidArgumentException;
use Illuminate\Contracts\Auth\Factory as FactoryContract;

class AuthManager implements FactoryContract
{
    use CreatesUserProviders;

    public function guard($name = null)
    {
        $name = $name ?: $this->getDefaultDriver();

        return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
    }
}

这里的guard方法就是工厂方法,AuthManager是工厂,它实现了guard方法,通过调用guard方法,可以得到不同的guard实例。当需要添加自定义的guard时,可以让AuthManager把自定义的产品类型(guard)添加进来,在调用时可以通过guard(“xxx”)取回自定义的guard实例。

1.2 建造者模式(Builder)

建造者模式将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。(对象创造过程进行抽离,把对象的创建变成标准步骤)。

关键点就是把步骤进行抽离,然后在面对不同类型对象的构建时,通过组合不同的步骤进行构建对象。

3 单例模式(Singleton)
单例模式的作用就是保证在整个应用程序的生命周期中,任何一个时刻,单例类的实例都只存在一个,同时这个类还必须提供一个访问该类的全局访问点。常见使用实例:数据库连接器;日志记录器(如果有多种用途使用多例模式);锁定文件。

<?php

/**
 * Singleton类
 */
class Singleton
{
    /**
     * @var Singleton reference to singleton instance
     */
    private static $instance;
    
    /**
     * 通过延迟加载(用到时才加载)获取实例
     *
     * @return self
     */
    public static function getInstance()
    {
        if (null === static::$instance) {
            static::$instance = new static;
        }

        return static::$instance;
    }

    /**
     * 构造函数私有,不允许在外部实例化
     *
     */
    private function __construct()
    {
    }

    /**
     * 防止对象实例被克隆
     *
     * @return void
     */
    private function __clone()
    {
    }

    /**
     * 防止被反序列化
     *
     * @return void
     */
    private function __wakeup()
    {
    }
}

由于PHP的运行模式的关系,单例设计模式并没有真正发挥到它应该起到的作用(只能锁定到请求作用域,无法应用到整个应用)。

2.1 适配器(Adapter)
适配器的存在,就是为了将已存在的东西(接口)转换成适合我们需要、能被我们所利用的东西。在现实生活中,适配器更多的是作为一个中间层来实现这种转换作用。比如电源适配器,它是用于电流变换(整流)的设备。

2.3 装饰模式(Decorator)
装饰器模式能够从一个对象的外部动态地给对象添加功能。

通常给对象添加功能,要么直接修改对象添加相应的功能,要么派生对应的子类来扩展,抑或是使用对象组合的方式。显然,直接修改对应的类这种方式并不可取。在面向对象的设计中,我们也应该尽量使用对象组合,而不是对象继承来扩展和复用功能。装饰器模式就是基于对象组合的方式,可以很灵活的给对象添加所需要的功能。装饰器模式的本质就是动态组合。动态是手段,组合才是目的。

常见的使用示例:Web服务层 —— 为 REST 服务提供 JSON 和 XML 装饰器。

2.3 依赖注入模式(Dependency Injection)

2.4 门面模式(Facade)
门面模式(Facade)又称外观模式,用于为子系统中的一组接口提供一个一致的界面。门面模式定义了一个高层接口,这个接口使得子系统更加容易使用:引入门面角色之后,用户只需要直接与门面角色交互,用户与子系统之间的复杂关系由门面角色来实现,从而降低了系统的耦合度。

2.5 流接口模式(Fluent Interface)

2.6 代理模式(Proxy)

3.1 责任链模式(Chain Of Responsibilities)
3.2 观察者模式(Observer)
3.3 迭代器模式(Iterator)