月度归档:2018年04月

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)