分类 Laravel 下的文章

我的个人博客:逐步前行STEP

1、执行单例测试

./vendor/bin/phpunit

2、执行指定单例测试文件

./vendor/bin/phpunit   tests/BlogTest.php

3、执行指定测试函数

./vendor/bin/phpunit  --filter testPostArticle

4、执行指定文件的指定测试函数

./vendor/bin/phpunit  --filter testPostArticle  tests/BlogTest.php

我的个人博客:逐步前行STEP

1、first

返回集合第一个通过指定测试的元素:

collect([1, 2, 3, 4])->first();

// 1
collect([1, 2, 3, 4])->first(function ($value, $key) {
    return $value > 2;
});

// 3

2、last

返回集合中,最后一个通过指定测试的元素:

collect([1, 2, 3, 4])->last(function ($value, $key) {
    return $value < 3;
});

// 2
collect([1, 2, 3, 4])->last();

// 4

3、keyBy

以指定键的值作为集合项目的键。如果几个数据项有相同的键,那在新集合中只显示最后一项:

$collection = collect([
    ['product_id' => 'prod-100', 'name' => 'desk'],
    ['product_id' => 'prod-200', 'name' => 'chair'],
]);

$keyed = $collection->keyBy('product_id');

$keyed->all();

/*
    [
        'prod-100' => ['product_id' => 'prod-100', 'name' => 'Desk'],
        'prod-200' => ['product_id' => 'prod-200', 'name' => 'Chair'],
    ]
*/
$keyed = $collection->keyBy(function ($item) {
    return strtoupper($item['product_id']);
});

$keyed->all();

/*
    [
        'PROD-100' => ['product_id' => 'prod-100', 'name' => 'Desk'],
        'PROD-200' => ['product_id' => 'prod-200', 'name' => 'Chair'],
    ]
*/

4、map

遍历整个集合并将每一个数值传入回调函数。回调函数可以任意修改并返回项目,形成修改过的项目组成的新集合

$collection = collect([1, 2, 3, 4, 5]);

$multiplied = $collection->map(function ($item, $key) {
    return $item * 2;
});

$multiplied->all();

// [2, 4, 6, 8, 10]

5、mapWithKeys

遍历整个集合并将每一个数值传入回调函数。回调函数返回包含一个键值对的关联数组

$collection = collect([
    [
        'name' => 'John',
        'department' => 'Sales',
        'email' => 'john@example.com'
    ],
    [
        'name' => 'Jane',
        'department' => 'Marketing',
        'email' => 'jane@example.com'
    ]
]);

$keyed = $collection->mapWithKeys(function ($item) {
    return [$item['email'] => $item['name']];
});

$keyed->all();

/*
    [
        'john@example.com' => 'John',
        'jane@example.com' => 'Jane',
    ]
*/

6、merge

合并数组进集合。数组「键」对应的数值会覆盖集合「键」对应的数值:

$collection = collect(['product_id' => 1, 'price' => 100]);

$merged = $collection->merge(['price' => 200, 'discount' => false]);

$merged->all();

// ['product_id' => 1, 'price' => 200, 'discount' => false]

7、only

返回集合中指定键的所有项目

$collection = collect(['product_id' => 1, 'name' => 'Desk', 'price' => 100, 'discount' => false]);

$filtered = $collection->only(['product_id', 'name']);

$filtered->all();

// ['product_id' => 1, 'name' => 'Desk']

8、pluck

获取集合中指定「键」所有对应的值

$collection = collect([
    ['product_id' => 'prod-100', 'name' => 'Desk'],
    ['product_id' => 'prod-200', 'name' => 'Chair'],
]);

$plucked = $collection->pluck('name');

$plucked->all();

// ['Desk', 'Chair']
$plucked = $collection->pluck('name', 'product_id');

$plucked->all();

// ['prod-100' => 'Desk', 'prod-200' => 'Chair']

9、reduce

reduce 方法将集合缩减到单个数值,该方法会将每次迭代的结果传入到下一次迭代

$collection = collect([1, 2, 3]);

$total = $collection->reduce(function ($carry, $item) {
    return $carry + $item;
});

// 6
第一次迭代时 $carry 的数值为 null;然而你也可以传入第二个参数进 reduce 以指定它的初始值:

$collection->reduce(function ($carry, $item) {
    return $carry + $item;
}, 4);

// 10

10、search

search 方法在集合内搜索指定的数值并返回找到的键。假如找不到项目,则返回 false

$collection = collect([2, 4, 6, 8]);

$collection->search(4);

// 1
$collection->search(function ($item, $key) {
    return $item > 5;
});

// 2

我的个人博客:逐步前行STEP

目前开发小程序,按需求要实现3种登陆方式:
1、微信授权登陆
2、账户密码登陆
3、手机号、验证码登陆
我使用laravel自带的Auth认证机制,通过attempt方法进行账户验证,但是默认的认证机制必须包含password字段,而我的第1、3种登陆方式都没有password字段,所以需要深入源码了解认证机制的实现,然后再进行修改。
首先,看看自带的Auth功能的LoginController怎么实现的:

class LoginController extends Controller
{
...
    use AuthenticatesUsers;
...
}

使用了trait:AuthenticatesUsers,AuthenticatesUsers中有一个login方法就是实现默认的登陆方式的方法:

    public function login(Request $request)
    {
        //这里是对登陆参数做表单验证
        $this->validateLogin($request);

        //这里是防止暴力破解,对同一个IP的接口调用次数做限制
        if ($this->hasTooManyLoginAttempts($request)) {
            $this->fireLockoutEvent($request);//限制访问
            return $this->sendLockoutResponse($request);//发回限制访问的响应
        }
        
        //验证登陆
        if ($this->attemptLogin($request)) {
            return $this->sendLoginResponse($request);//返回登陆成功的响应
        }

        //登录失败,失败次数++,防止暴力破解
        $this->incrementLoginAttempts($request);

        // 返回登陆失败的响应
        return $this->sendFailedLoginResponse($request);
    }

这里的重点在于:attemptLogin方法的调用,这才是关键的一步:登陆验证

    protected function attemptLogin(Request $request)
    {
        return $this->guard()->attempt(
            $this->credentials($request), $request->filled('remember')
        );
    }

再看guard函数:

    /**
     * Get the guard to be used during authentication.
     *
     * @return \Illuminate\Contracts\Auth\StatefulGuard
     */
    protected function guard()
    {
        return Auth::guard();
    }

注释说明返回

IlluminateContractsAuthStatefulGuard

,找到该文件发现这是一个接口文件,定义 了attempt方法,直接搜索

implements StatefulGuard

看哪个类实现了该接口,找到了

IlluminateAuthSessionGuard

以及其中的attempt方法:

    /**
     * Attempt to authenticate a user using the given credentials.
     *
     * @param  array  $credentials
     * @param  bool   $remember
     * @return bool
     */
    public function attempt(array $credentials = [], $remember = false)
    {
        $this->fireAttemptEvent($credentials, $remember);
        //这里获取了用户信息
        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
        //校验用户密码
        if ($this->hasValidCredentials($user, $credentials)) {
            $this->login($user, $remember);

            return true;
        }

        $this->fireFailedEvent($user, $credentials);

        return false;
    }

获取用户信息:

 $user = $this->provider->retrieveByCredentials($credentials);

和校验用户密码:

$this->hasValidCredentials($user, $credentials)

就是Auth认证的核心了,首先看怎么获取用户信息:

IlluminateAuthSessionGuard

的构造函数可见在实例化SessionGuard的时候传入了UserProvider $provider:

    public function __construct($name,
                                UserProvider $provider,
                                Session $session,
                                Request $request = null)
    {
        $this->name = $name;
        $this->session = $session;
        $this->request = $request;
        $this->provider = $provider;
    }

直接搜索

new SessionGuard

找到

IlluminateAuthAuthManager

中的:

    /**
     * Create a session based authentication guard.
     *
     * @param  string  $name
     * @param  array  $config
     * @return \Illuminate\Auth\SessionGuard
     */
    public function createSessionDriver($name, $config)
    {
        //看这里,通过$config['provider']创建了provider
        $provider = $this->createUserProvider($config['provider'] ?? null);
        $guard = new SessionGuard($name, $provider, $this->app['session.store']);
        if (method_exists($guard, 'setCookieJar')) {
            $guard->setCookieJar($this->app['cookie']);
        }

        if (method_exists($guard, 'setDispatcher')) {
            $guard->setDispatcher($this->app['events']);
        }

        if (method_exists($guard, 'setRequest')) {
            $guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
        }

        return $guard;
    }

继续跟踪到IlluminateAuthAuthManager使用的trait:IlluminateAuthCreatesUserProviders中的createUserProvider:

    public function createUserProvider($provider = null)
    {
        if (is_null($config = $this->getProviderConfiguration($provider))) {
            return;
        }

        if (isset($this->customProviderCreators[$driver = ($config['driver'] ?? null)])) {
            return call_user_func(
                $this->customProviderCreators[$driver], $this->app, $config
            );
        }

        switch ($driver) {
            case 'database':
                return $this->createDatabaseProvider($config);
            case 'eloquent':
                return $this->createEloquentProvider($config);
            default:
                throw new InvalidArgumentException(
                    "Authentication user provider [{$driver}] is not defined."
                );
        }
    }

对照config/auth.php中的provider驱动配置,默认是eloquent,也就是会执行:

return $this->createEloquentProvider($config);

跟棕到该方法:

    protected function createEloquentProvider($config)
    {
        return new EloquentUserProvider($this->app['hash'], $config['model']);
    }

可以确定在 IlluminateAuthSessionGuard的attempt函数中的provider就是IlluminateAuthEloquentUserProvider,找到retrieveByCredentials函数:

    public function retrieveByCredentials(array $credentials)
    {
        if (empty($credentials) ||
           (count($credentials) === 1 &&
            array_key_exists('password', $credentials))) {
            return;
        }
        $query = $this->createModel()->newQuery();

        foreach ($credentials as $key => $value) {
            if (Str::contains($key, 'password')) {
                continue;
            }

            if (is_array($value) || $value instanceof Arrayable) {
                $query->whereIn($key, $value);
            } else {
                $query->where($key, $value);
            }
        }

        return $query->first();
    }

在这里根据除密码之外的其它参数查询出了用户数据。
回到 IlluminateAuthSessionGuard,再看:

    /**
     * Determine if the user matches the credentials.
     *
     * @param  mixed  $user
     * @param  array  $credentials
     * @return bool
     */
    protected function hasValidCredentials($user, $credentials)
    {
        return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
    }

调用了IlluminateAuthEloquentUserProvider的validateCredentials方法:


    /**
     * Validate a user against the given credentials.
     *
     * @param  \Illuminate\Contracts\Auth\Authenticatable  $user
     * @param  array  $credentials
     * @return bool
     */
    public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];
        //对比加密后的密码是否和数据库中的相同
        return $this->hasher->check($plain, $user->getAuthPassword());
    }

最终,我们确认只要在EloquentProvider中的validateCredentials修改为自己的验证方式就可以实现需求了,可是直接修改源码还是不安全,可能会导致其它不可预测的问题,毕竟没有深入研究,还是保险一点,增加一个provider,写一个新的validateCredentials方法,会是更好的选择。
新建一个NewEloquentUserProvider继承EloquentUserProvider,重写validateCredentials:

    public function validateCredentials(Authenticatable $user, array $credentials)
    {
        if(array_key_exists('openid',$credentials)){
            //openid登陆
            $openid = $credentials['openid'];
            if($user->getAuthOpenid() == $openid) return true;

        }elseif(array_key_exists('password',$credentials)){
            //Phone、password登陆
            $plain = $credentials['password'];
            return $this->hasher->check($plain, $user->getAuthPassword());

        }else{
            //Phone、code登陆
           $authCode  = Cache::get("login_verification_code_".$credentials['code']);
            if($authCode && $authCode == $credentials['code']) return true;

        }

        return false;
    }

实现三种方式的登陆验证,然后在 trait:IlluminateAuthCreatesUserProviders中的createUserProvider函数的switch分支里新增一个case,并返回NewEloquentUserProvider的实例,再将config/auth.php中的providers.users.driver配置改为该case的值即可。

我的个人博客:逐步前行STEP

laravel中的任务调度可以不将每条命令都写入crontab,便于管理维护,而且可以基于laravel框架环境运行,而不需写独立的脚本执行,非常方便,但是最小的执行间隔也是一分钟,要想达到每秒执行的效果,就要借助shll脚本了,在shell脚本里循环60次每秒执行schedule:run命令,就达到了秒级定时任务的效果,以下是我使用的shell脚本:

#!/bin/bash
step=1 #间隔的秒数

for (( i = 0; i < 60; i=(i+step) )); do
    /usr/local/php/bin/php /home/www/blog/artisan schedule:run
    sleep $step
done

exit 0

我的个人博客:逐步前行STEP

最近有一个商城项目中有统计商品点击量和艺术家访问量的需求,但又不想改动太多原来的代码,而点击与访问这两个动作是有明确触发点的,正好可以用laravel中的事件系统来做,在点击和访问对应的函数中产生这俩事件,监视器获取到之后,再将记录保存到数据库中,并更新计数。

1、在

appProvidersEventServiceProvider

中注册监听器:

    /**
     * The event listener mappings for the application.
     *
     * @var array
     */
    protected $listen = [
        ......
        'App\Events\Statistics' => [
            'App\Listeners\BehavioralStatistics',
        ],
        ......
    ];

2、
执行

php artisan event:generate

生成事件类与监听类

3、定义事件

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class Statistics
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $user;
    public $obj;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct($user,$obj)
    {
        $this->user = $user;
        $this->obj = $obj;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('channel-name');
    }
}

4、定义监听器:

<?php

namespace App\Listeners;

use App\Events\Statistics;
use App\System\StaticsView;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Log;

class BehavioralStatistics
{
    /**
     * Create the event listener.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Handle the event.
     *
     * @param  Statistics  $event
     * @return void
     */
    public function handle(Statistics $event)
    {
        $obj_class = get_class($event->obj);
        $statics_view = new StaticsView;

        switch($obj_class){
            case "App\\User":
                $statics_view->statics_type = 'user';

                break;
            case "App\\Production":
                $statics_view->statics_type = 'production';

                break;
        }

        $statics_view->ip = request()->getClientIp();;
        $statics_view->time_local = 0;
        $statics_view->statics_id = $event->obj->id;
        $statics_view->save();
    }
}

5、触发事件:

event(new Statistics($user,$production));