Laravel查询在死锁发生时返回空集合

想象一下,您有两个进程同时执行以下PHP代码,将显示什么结果?

\DB::transaction(function (){
   dump(User::where('id',1)->lockForUpdate()->first());
});

令人惊讶的是,一个进程打印正常的查询结果,而另一个进程打印一个空集合(数组)。 返回空集合的代码并未按照我们想的那样触发死锁异常!然而,如果您查阅了transaction方法的源代码,则会发现在发现死锁之后,该方法会自动重试事务。 而且当死锁的数量达到设定值时,最终死锁将被作为异常抛出。 但是,当前这个实际发生死锁的查询看起来却很正常,因为它返回了一个空集合!

这个意外返回的空数组和未抛出的死锁异常实际上是一个古老且棘手的PHP-PDO bug。 在7.4.13发布之前,它已经广泛存在于许多PHP版本中。 有许多与此相关的讨论,例如:

  1. https://bugs.php.net/bug.php?id=76742
  2. https://github.com/php/php-src/pull/5937
  3. https://github.com/php/php-src/pull/6203
  4. https://github.com/php/php-src/commit/b03776adb5bbb9b54731a44377632fcc94a59d2f

在PHP7.4.13之前,你几乎没有办法直接检测到此错误。 切勿尝试打开PDO :: ATTR_EMULATE_PREPARES来解决此问题! 因为此选项将使您所有的PDO查询结果都变成字符串,数据类型的丢失将会导致更严重的后果。

因此,唯一可行的方法是手动编译已修复的PHP源代码,或安装新的预编译版本的PHP7.4.13。

Laravel Union操作后排序错误的问题 Fix the wrong order after using union method in Laravel query

解决方法,在两个子查询中分别加上limit即可。可以使用较大的数确保所有记录返回。

The solution is to use the limit method on the two subqueries. You can use a larger number to ensure that all records are returned.

$orders1 = Order::where('id', '=', $user->id)
    ->where('status', '!=', 0)
    ->orderBy('id', 'asc')
    ->limit(1e8);

$orders2 = Order::where('id', '=', $user->id)
    ->where('status', '=', 0)
    ->orderBy('created_at', 'desc')
    ->limit(1e8)
    ->unionAll($orders1)
    ->get();

 

Laravel 5.5在浏览器中预览Notification渲染 Previewing Laravel Notification In Browser

对于Mail,我们可以通过下面的代码在浏览器中预览Mailables

For Mail, we can preview Mailables in the browser with the following code.

Route::get('/mailable', function () {
    $invoice = App\Invoice::find(1);
    return new App\Mail\InvoicePaid($invoice);
});

然而Notification类中toMail返回的MailMessage实例,Laravel文档中并没有给出直接方法在浏览器中预览,我们可以采取下面的方法。

However, the MailMessage instance returned by toMail method in the Notification class does not give a direct method to preview in the browser in the Laravel document. We can use the following code.

Route::get('/mailable', function () {
    $message = (new \App\Notifications\FooNotification()->toMail("bar");
    return app()->make(\Illuminate\Mail\Markdown::class)->render($message->markdown, $message->data());
});

在Laravel5.8及以上,MailMessage实现了Renderable接口(PR in Github),所以可以直接return MailMessage实例作为响应了。

In Laravel 5.8 and above, MailMessage implements the Renderable interface (PR in Github), so you can directly return the MailMessage instance as a response.

 

在mac上使用vmbox映射本地目录开发laravel时,解决storage目录Permission denied的问题

我目前使用vmbox中的ubuntu虚拟机共享代码目录来开发laravel,在这期间遇到一个古怪的问题,就是发现storage目录下面的一些文件虚拟机里的php-fpm貌似没有权限(Permission denied),包括但不限于storage/app,storage/logs等。

而当我在mac中把这些目录的权限改为777后发现并不能解决问题。其中在storage/framework/cache中,php-fpm的确拥有了在这个目录创建二级目录的权限,但是貌似在他自己创建的目录中,php-fpm又并没有w权限了。

后来登录到ubuntu中测试发现,php-fpm在cache文件夹创建了缓存索引文件夹,所有者居然是root的,而且的确没有其他人的w权限。后来又经过其他测试我发现,在vmbox的共享目录中,无论你在虚拟机里使用哪个用户创建什么文件,它的所有者都是root:root,权限也都如下图所示。

后来这个问题用了很多方法,包括mac这边设置用户组,ubuntu设置用户组,但都没有很好的解决。后来想了一个终极办法,就是让php-fpm以root的权限运行。解决方案参考:https://onlyke.com/html/883.html

让php-fpm以root的权限运行

注意,本方法仅用于开发环境来解决一些奇怪问题或者方便使用,请勿在生产环境让php-fpm以root身份运行,否则一切后果自负。

  1. 编辑/etc/php/7.0/fpm/pool.d/www.conf,把user和group修改为root
    ; Unix user/group of processes 
    ; Note: The user is mandatory. If the group is not set, the default user's group ; will be used. 
    user = root 
    group = root
    
  2. 编辑/lib/systemd/system/php7.0-fpm.service,在ExecStart的–nodaemonize前面加上–allow-to-run-as-root,就像下面这样
    ExecStart=/usr/sbin/php-fpm7.0 --allow-to-run-as-root --nodaemonize...
  3. 执行命令
    systemctl daemon-reload
  4. 重启php-fpm即可
    service php7.0-fpm restart
  5. 最后,可以通过ps命令来查看效果
    ps auwx | grep php

    可以看到,php-fpm已经以root身份运行了。

Nginx+PHP-fpm中开启日志,error_log的有关问题

要保障PHP在error_log中输出日志,要确保以下设置为正确的值

php.ini中

; Common Values:
;   E_ALL (Show all errors, warnings and notices including coding standards.)
;   E_ALL & ~E_NOTICE  (Show all errors, except for notices)
;   E_ALL & ~E_NOTICE & ~E_STRICT  (Show all errors, except for notices and coding standards warnings.)
;   E_COMPILE_ERROR|E_RECOVERABLE_ERROR|E_ERROR|E_CORE_ERROR  (Show only errors)
; Default Value: E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED
; Development Value: E_ALL
; Production Value: E_ALL & ~E_DEPRECATED & ~E_STRICT
; http://php.net/error-reporting
error_reporting = E_ALL & ~E_NOTICE & ~E_STRICT

...

; Besides displaying errors, PHP can also log errors to locations such as a
; server-specific log, STDERR, or a location specified by the error_log
; directive found below. While errors should not be displayed on productions
; servers they should still be monitored and logging is a great way to do that.
; Default Value: Off
; Development Value: On
; Production Value: On
; http://php.net/log-errors
log_errors = On

php-fpm.conf中error_log覆盖了php.ini中的设置

; Error log file
; If it's set to "syslog", log is sent to syslogd instead of being written
; in a local file.
; Note: the default prefix is /var
; Default Value: log/php-fpm.log
error_log = /var/log/php7.0-fpm.log

pool.d/www.conf中,确保开启输出(非常重要)

; Redirect worker stdout and stderr into main error log. If not set, stdout and
; stderr will be redirected to /dev/null according to FastCGI specs.
; Note: on highloaded environement, this can cause some delay in the page
; process time (several ms).
; Default Value: no
catch_workers_output = yes

 

Laravel 5.3 让用户使用明文密码并登陆

虽然不推荐使用明文密码,但是我还是发一篇教程

使用的方法还是自定义Hash,只不过我们在处理中不进行hash运算即可,参考https://onlyke.com/html/829.html

首先,新建自定义的NonHasher

<?php
namespace App\Libraries\Hashing;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
 
class NonHasher implements HasherContract{
 
    /**
     * Hash the given value.
     *
     * @param  string  $value
     * @param  array   $options
     * @return string
     */
    public function make($value, array $options = []){
        return $value;
    }
 
    /**
     * Check the given plain value against a hash.
     *
     * @param  string  $value
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function check($value, $hashedValue, array $options = []){
        if (strlen($hashedValue) === 0) {
            return false;
        }
        return strcmp($value,$hashedValue) == 0;
    }
 
    /**
     * Check if the given hash has been hashed using the given options.
     *
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function needsRehash($hashedValue, array $options = []){
        if (strlen($hashedValue) === 0) {
            return false;
        }
        return $hashedValue;
    }
}

然后,在AuthServiceProvider中注入

#app\Providers\AuthServiceProvider.php

namespace App\Providers;

use Auth;
use Illuminate\Auth\EloquentUserProvider;
use App\Libraries\Hashing\NonHasher;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        //使用自定义的Hash来处理密码
        Auth::provider('non', function($app, array $config) {
            return new EloquentUserProvider(new NonHasher,$config['model']);
        });
    }
}

当然,最后别忘了设置config\auth.php下面的providers中driver为你上面在Auth::provider中声明的名字

/*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'non',
            'model' => App\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

Laravel 5.3 使用自定义的哈希(Hash)函数来处理密码

为了兼容minecraft的authme密码hash方式,我们不能使用laravel自带的BcryptHash。所以我们需要对登录流程进行一些修改。

首先我们来看原来的BcryptHash是在哪里使用的

#vendor\laravel\framework\src\Illuminate\Auth\CreatesUserProviders.php

    /**
     * Create an instance of the Eloquent user provider.
     *
     * @param  array  $config
     * @return \Illuminate\Auth\EloquentUserProvider
     */
    protected function createEloquentProvider($config)
    {
        return new EloquentUserProvider($this->app['hash'], $config['model']);
    }

这里我们发现当创建默认的EloquentUserProvider时,laravel自带hash,也就是Bcrypt被作为参数传递了,所以我们就要想办法让这里不使用自带hash,而在这里直接更改是不行的,因为这是laravel的源码,我们是不能修改的。

我们转而把注意力放在自定义UserProvider上,自定义之后我们当然就能使用自己的hash方式了。下面是一个例子(http://laravelacademy.org/post/5974.html

#app\Providers\AuthServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\Facades\Auth;
use App\Extensions\RiakUserProvider;
use Illuminate\Support\ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register any application authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Auth::provider('riak', function($app, array $config) {
            // Return an instance of Illuminate\Contracts\Auth\UserProvider...
            return new RiakUserProvider($app->make('riak.connection'));
        });
    }
}

但是这时候我们发现,其实我们完全还可以在这里继续使用EloquentUserProvider,因为我们的目的只是更改hash方式,只需要在上面实例化EloquentUserProvider的地方,将我们自己的Hash类作为第一个参数传递进去就好了。

首先,我们先写好我们自己的Hash类,根据EloquentUserProvider的源码,我们发现我们的Hash类必须是Illuminate\Contracts\Hashing\Hasher契约的实现,契约的源码如下

interface Hasher
{
    /**
     * Hash the given value.
     *
     * @param  string  $value
     * @param  array   $options
     * @return string
     */
    public function make($value, array $options = []);

    /**
     * Check the given plain value against a hash.
     *
     * @param  string  $value
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function check($value, $hashedValue, array $options = []);

    /**
     * Check if the given hash has been hashed using the given options.
     *
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function needsRehash($hashedValue, array $options = []);
}

我们只需要补全上面的方法,实现这个接口即可,下面是我们自定义的AuthmeHasher

namespace App\Libraries\Hashing;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;

class AuthmeHasher implements HasherContract{

    /**
     * Hash the given value.
     *
     * @param  string  $value
     * @param  array   $options
     * @return string
     */
    public function make($value, array $options = []){
        $p1 = '$SHA';
        $p2 = str_random(16);
        $p3 = hash('sha256', hash('sha256', $value) . $p2);
        $hash = join('$',[$p1,$p2,$p3]);
        return $hash;
    }

    /**
     * Check the given plain value against a hash.
     *
     * @param  string  $value
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function check($value, $hashedValue, array $options = []){
        if (strlen($hashedValue) === 0) {
            return false;
        }

        $parts = explode('$', $hashedValue);
        return count($parts) === 4
        && $parts[3] === hash('sha256', hash('sha256', $value) . $parts[2]);
    }

    /**
     * Check if the given hash has been hashed using the given options.
     *
     * @param  string  $hashedValue
     * @param  array   $options
     * @return bool
     */
    public function needsRehash($hashedValue, array $options = []){
        if (strlen($hashedValue) === 0) {
            return false;
        }
        return $hashedValue;
    }
}

这里由于不知道needsRehash方法该如何写,所以我直接返回了原先的hashedValue。具体这个文件的写法,我们也可以参考laravel默认的vendor\laravel\framework\src\Illuminate\Hashing\BcryptHasher.php

接下来就是最关键的部分了,我们将自己的AuthmeHash注入到EloquentUserProvider

#app\Providers\AuthServiceProvider.php

namespace App\Providers;

use Auth;
use Illuminate\Auth\EloquentUserProvider;
use App\Libraries\Hashing\AuthmeHasher;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        //使用authme的Hash来处理密码
        Auth::provider('authme', function($app, array $config) {
            return new EloquentUserProvider(new AuthmeHasher,$config['model']);
        });
    }
}

当然,最后别忘了设置config\auth.php下面的providers中driver为你上面在Auth::provider中声明的名字

/*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'authme',
            'model' => App\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

 

在最后还是说一句,学习Laravel阅读源码还是关键,文档的作用有的时候比较有局限性。本篇文章参考了http://blueve.me/archives/898的一些信息