0%

不做动机揣测,少做价值判断

Laravel环境检测的方法是很容易的,因为大家都知道Laravel中使用了Dotenv来做文件配置,这就牵扯到了我们的都很熟悉的.env。

比如一个典型的.env是:

1
2
3
4
5
6
7
8
9
10
11
12
APP_ENV=local
APP_DEBUG=true
APP_KEY=base64:3csTuw5O1me1PF4j9xErbUR+seyH1xdf6uRfvjuQ22Q=
APP_URL=http://localhost

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=db_name
DB_USERNAME=db_user
DB_PASSWORD=db_pass
//...省略

今天我的这篇分享主要介绍两种区分不同环境的方法。

不同环境维护自己的.env文件

这种办法也是Laravel默认的办法,也就是开发环境、测试环境和生产环境各自维护自己的.env文件,也就是说.env不要加到版本控制系统中,通过不同的配置可以做到环境的区分,比如:

开发环境

1
2
APP_ENV=dev
//...省略

测试环境

1
2
APP_ENV=staging
//...省略

生产环境

1
2
APP_ENV=production
//...省略

这样,在我们的代码中即可以通过判断APP_ENV来判断是哪种环境。但是这种办法我个人是不太喜欢的,原因有以下几点:

  • 环境的判断过多的写到代码里面,维护起来很麻烦
  • 如果某次上线需要修改配置文件,则每次上线代码还需要到线上机器修改.env文件,部署起来很麻烦

不同环境加载自己的.env.文件

这里面的.env.文件延伸开来就是.env.dev、.env.test和.env.prod,比如开发环境会自动加载.env.dev,依此类推,那么如果是这样的话,不同环境的机器又怎么知道加载哪个文件呢?其实这里面还是耍了点小聪明,别忘了我们有php.ini

  • 1.在php.ini中追加一行配置
    当然开发、测试和生产环境中env所对应的值也需要不一样
    1
    2
    3
    4
    5
    6
    //开发环境
    env=dev
    //测试环境
    env=staging
    //生产环境
    env=production
  • 2.加载不同的配置文件
    在bootstrap/app.php文件中添加如下判断,在这里将通过获取php.ini中的env的值,然后从而加载不同的配置文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    //...省略
    $env = get_cfg_var('env');
    $env = !empty($env) ? $env : 'production';
    if(!defined('APP_MODE')){
    define('APP_MODE', $env);
    }
    $app->loadEnvironmentFrom('.env.'.$env);

    return $app;
  • 3.新建.env.dev、.env.staging和.env.production

好,就到此为止了,当然虽然我非常喜欢第二种办法,但第二种毕竟需要对线上机器做一次大的改动,需要运维同学支持下。但是也需要具体情况具体来选择吧。

http://xuwenzhi.com/2016/07/31/laravel%E4%B9%8B%E5%BC%80%E5%8F%91%E3%80%81%E6%B5%8B%E8%AF%95%E5%92%8C%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E5%8C%BA%E5%88%86/

能够击败我的不会是孤独,而是试图摆脱孤独。

第1步 - 学习一门语言

谈到选择语言有很多选择。我已经将它们分成几类,以便你更容易做出决定。对于刚开始进入后端开发的初学者,我建议你选择任何脚本语言,因为它们有很多需求,它可以让你快速起步。如果你有一些前端知识,你可能会发现Node.js更容易,再加上有一个很大的就业市场。

如果你已经在做后端开发并且知道一些脚本语言,我建议你不要选择另一种脚本语言,并从“功能”或“多参数”部分中选择一些。
例如,如果你已经在使用PHP或Node.js,请不要使用Python或Ruby,而应尝试使用Erlang或Golang。它肯定会帮助你延伸思维,并开启你的思想到新的视野。

第2步 - 练习你学到的东西

没有比实践更好的学习方式。一旦你选择了你的语言,并且对这些概念有了基本的了解,就可以使用它们。尽你所能制作尽可能多的小应用程序。尽你所能制作尽可能多的小应用程序:

在bash中实现一些你自己使用的命令尝试实现 ls 的功能
编写一个命令,为你提供JSON格式的目录结构,例如 jsonify dir-name 给你一个带有 dir-name 内结构的JSON文件
编写一个从上面的步骤读取JSON的命令并创建目录结构
想想你每天都在做的一些任务,并尝试将其自动化

第3步 - 学习软件包管理器

了解了该语言的基础知识并制作了一些示例应用程序后,请了解如何使用你选择的语言的软件包管理器。软件包管理器可帮助你在应用程序中使用外部库,并分发你的库供其他人使用。

如果你选择了PHP,你将不得不学习 Composer,Node.js 有 NPM 或 Yarn,Python 有 Pip,Ruby 有 RubyGems。无论你选择什么,请继续学习如何使用其包管理器。

第4步 - 标准和最佳实践

每种语言都有自己的标准和做事的最佳实践。研究他们为你挑选的语言。例如 PHP 有 PHP-FIG 和 PSR 。使用 Node.js 有许多不同的社区驱动指南,其他语言也有相同的指导。

第5步 - 安全

请务必阅读有关安全的最佳做法。阅读 OWASP 指南并了解不同的安全问题以及如何以你选择的语言避免它们。

第6步 - 练习

现在你已经掌握了语言,标准和最佳实践的基础知识,安全性以及如何使用软件包管理器。现在开始创建一个包并分发给其他人使用,并确保遵循你迄今为止学到的标准和最佳实践。例如,如果你选择了PHP,那么你将在Packagist上发布它,如果你选择了Node.js,那么你将在Npm注册源中发布它,等等。

一旦你完成了,在Github上搜索一些项目,并在某些项目中打开一些pull请求。对此的一些想法:

重构并实施你学到的最佳实践
查看未解决的问题并尝试解决
添加任何附加功能

第7步 - 了解测试

测试有几种不同的测试类型。了解这些类型它们的目的是什么。了解如何在应用程序中编写单元测试和集成测试。另外,了解不同的测试术语,如 mocks, stubs 等。

第8步 - 实践

对于练习,继续编写单元测试,以完成目前为止所做的实际任务,特别是你在步骤6中所做的练习。

还要学习和计算你编写的测试的覆盖率。

第9步 - 了解关系数据库

了解如何将数据保存在关系数据库中。在你选择要学习的工具之前,请先了解不同的数据库术语,例如键,索引,规范化等。

这里有几个选项。但是,如果你学习一个,其他的应该相当容易。你想学习的是MySQL,MariaDB(大部分是相同的,是MySQL的分支)和PostgreSQL。选择MySQL开始。

第十步 - 实践时间

现在是时候把你所学到的一切都用到这里去了。

使用你迄今为止学到的所有内容创建一个简单的应用程序。可以选择任何想法​​,也许创建一个简单的博客应用程序,并实现其中的以下功能。

用户帐户 - 注册和登录
注册用户可以创建博客文章
用户应该能够查看他创建的所有博客文章
他们应该能够删除他们的博客文章
确保用户只能看到他的个人博客帖子,而不能看到他人
编写应用程序的单元/集成测试
你应该为查询应用索引。分析查询以确保正在使用索引

第11步 - 了解一个框架

根据你选择的项目和语言,你可能需要也可能不需要框架。每种语言都有几个不同的选项,继续看看你选择的语言有哪些选项可供选择,然后选择相关的一个。

如果你选择了PHP,我会建议你使用 Laravel或Symfony,如果是为框架的话,使用Lumen或Slim。如果你选择Node.js,有几种不同的选择,但突出的是Express.js。

第12步 - 实践时间

为了实现此步骤,请将你在 步骤10 中创建的应用程序转换为使用你选择的框架。还要确保移植包括测试在内的所有内容。

第13步 - 学习NoSQL数据库

首先了解它们是什么,它们与关系数据库有何不同以及为什么它们是需要的。有几种不同的选择,研究一点看看,并比较它们的特点和差异。你可以选择的一些常用选项是Rdeis,MongoDB,Cassandra,RethinkDB和Couchbase。如果你必须选择一个,请使用Redis。

第14步 - 缓存

了解如何在你的应用程序中实施应用程序级缓存。了解如何使用Redis或Memcached并在你在 步骤12 中创建的应用程序中实施缓存。

第15步 - 创建RESTful API

了解REST并学习如何制作RESTful API,并确保从 Roy Fielding 的原始文章中阅读关于REST的部分。如果他们说REST仅适用于HTTP API,请确保你能够与其他人对战。

第16步 - 了解不同的身份验证方法

了解不同的身份验证和授权方法。你应该知道他们是什么,他们有什么不同以及什么时候偏好某一个

OAuth - 开放认证
基本认证
令牌认证
JWT - JSON Web令牌
OpenID

第17步 - 消息代理

了解消息代理并了解何时以及为何使用它们。有多种选择,但突出的是 RabbitMQ 和 Kafka。现在学习如何使用RabbitMQ,如果你想选择一个。

第18步 - 搜索引擎

随着应用程序的增长,对关系数据库或NoSQL数据库的简单查询不会将其切断,你将不得不求助于搜索引擎。有多种选择,每种选择都有自己的差异。比如 Solr, Sphinx, ElasticSearch,Xapian等。

第19步 - 了解如何使用Docker

无论你是在复制与生产环境相同的环境,还是保持操作系统清洁或加快你的编码,测试或部署,Docker都可以在开发过程中大大方便你的工作。在这一步中,继续学习如何使用Docker。

第20步 - 关于Web服务器的知识

如果你已经走到这么远,你可能不得不在前面的步骤中使用服务器。这一步主要是找出不同Web服务器之间的差异,了解限制和不同的可用配置选项,以及如何最好地利用这些限制编写应用程序。

第21步 - 了解如何使用Web Sockets

虽然不是必需的,但在工具带中有这些知识是有益的。学习如何使用 Web sockets 编写实时Web应用程序并使用它创建一些示例应用程序。你可以在上面制作的博客应用程序中使用它来实现博客文章列表中的实时更新。

第22步 - 学习GraphQL

学习如何使用GraphQL制作API。了解它与REST的不同之处,以及它为什么被称为 REST 2.0。

第23步 - 研究Graph数据库

Graph 模型代表了一种处理数据中关系的非常灵活的方式,图数据库为其提供了快速高效的存储,检索和查询。学习如何使用 Neo4j或 OrientDB。

第24步 - 保持探索

一旦你开始学习和练习,你一定会遇到我们在这个路线图中没有涉及的东西。只要保持开放的心态和对新事物的健康渴望。

记住关键是要尽可能多地练习。它在开始时看起来更加可怕,你可能会觉得你并没有抓住任何东西,但这是正常的,随着时间的推移,你会觉得自己越来越好。

好了,就这么多。感谢阅读。

https://phpcasts.org/posts/modern-backend-developer-in-2018

做你害怕做的事情。
我经常发现自己希望在Laravel应用程序中获得更多关于模型的结构。

默认情况下,模型位于 App 命名空间内,如果你正在处理大型应用程序,这可能会变得非常难以理解。所以我决定在 App\Models 命名空间内组织我的模型。

更新用户模型

要做到这一点,你需要做的第一件事就是将 User 模型移动到 app/Models 目录并相应地更新命名空间。

这要求你更新引用 App\User 类的所有文件。

第一个是 config/auth.php:

1
2
3
4
5
6
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class, // 修改这里
],
],

第二个是 config/services.php 文件:

1
2
3
4
5
'stripe' => [
'model' => App\Models\User::class, // 修改这里
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
],

最后,修改 database/factories/UserFactory.php 文件:

1
2
3
$factory->define(App\Models\User::class, function (Faker $faker) {
...
});

生成模型

现在我们已经改变了 User 模型的命名空间,但是如何生成新的模型。正如我们所知,默认情况下它们将被放置在 App 命名空间下。

为了解决这个问题,我们可以扩展默认的 ModelMakeCommand :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace App\Console\Commands;
use Illuminate\Foundation\Console\ModelMakeCommand as Command;
class ModelMakeCommand extends Command
{
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return "{$rootNamespace}\Models";
}
}

并通过将以下内容添加到 AppServiceProvider 中来覆盖服务容器中的现有绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Console\Commands\ModelMakeCommand;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
$this->app->extend('command.model.make', function ($command, $app) {
return new ModelMakeCommand($app['files']);
});
}
}

以上就是需要修改的。现在我们可以继续生成模型,就像我们在我们的终端中使用的一样:php artisan make:model Order,它们将位于 App\Models 命名空间中。

希望你能使用它!

https://phpcasts.org/posts/how-to-organize-your-laravel-models

说用心没有用的人,以为光有钱就有用了么。

假设我们要完成一个保存文章的功能,如果采用函数编程的方式,大概会是下面这个样子:

1
2
3
4
function saveArticle($title, $content, $categoryId)
{
// ...
}

每个参数代表一个属性,但带来一个问题,参数列表会变得很长。此时采用对象编程的技术会是个好方法:

1
2
3
4
5
6
7
8
9
10
11
class Article
{
var $title;
var $content;
var $categoryId;

function save()
{
// ...
}
}

在这里,原来的方法参数都转换为以对象的属性方式存在,从而大大降低了方法的参数数量。多数时候这个方法是不错的,不过并不是所有的参数都适合以对象属性的方式存在,区分的原则是,看这些参数是属于对象的内在属性还是外在特征,如果不加区分的统统转换为对象属性,这会让对象本身失去意义。

举个例子,比如说我们的Article对象还有一个名为find的方法,使用它时可能会涉及limit, offset, order等参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Article
{
var $title;
var $content;
var $categoryId;

var $limit = 10;
var $offset = 0;
var $order = 'created DESC';

function save()
{
// ...
}

function find($categoryId)
{
// ...
}
}

如上所示,一旦我们把limit,offset,order等参数转换为对象的属性,这个对象本身就变得不伦不类了,因为虽然title,content,categoryId可以算作是对象的内在属性,而limit,offset,order却不是,顶多也只能算作是外部特征,不加区分的结果基本是噩梦的开始。所以,还是老老实实的把外在特征放到方法的参数里好些(更严格的说,find方法应该是一个static类型的方法,不过文本就是一个简单的说明,不必细究):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Article
{
var $title;
var $content;
var $categoryId;

function save()
{
// ...
}

function find($categoryId, $limit = 10, $offset = 0, $order = 'created DESC')
{
// ...
}
}

可惜如此一来又出现了文章开头所说的问题,find方法的参数太多了,缺乏可读性,所以还得重构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Article
{
var $title;
var $content;
var $categoryId;

function save()
{
// ...
}

function find($categoryId, $options = array())
{
$default = array(
'limit' => 10,
'offset' => 0,
'order' => 'created DESC'
);

$options = array_merge($default, (array)$options);

// ...
}
}

看上去还不错,调用时可以使用类似下面的方法:

1
$article->find(123, array('limit' => 20));

简单的说,就是用数组方式的参数,来模拟一种类似关键字参数的效果,采用这种方式的话,可以通过数组的键名来描述参数的作用,从而增加代码的可读性。类似这样的重构方法在CakePHP等项目里被大量使用,仔细体会,以后再写代码的时候就可以本能的写出高可读性的代码来。

补充:相对array_merge来说,使用$options += $default;也是不错的选择。另外,如果options的逻辑很复杂的话,也可以不用数组的方式,转而使用一个专门的对象来封装逻辑操作。

https://www.awaimai.com/869.html

学着把眼泪像珍珠一样收藏,把眼泪都贮存在成功的那一天流淌,那一天,哪怕流它个大海汪洋。

The mcrypt extension has been abandonware for nearly a decade now, and was also fairly complex to use. It has therefore been deprecated in favour of OpenSSL, where it will be removed from the core and into PECL in PHP 7.2.

加密基础

加密算法一般分为两种:对称加密算法和非对称加密算法。

  • 对称加密
    对称加密算法是消息发送者和接收者使用同一个密匙,发送者使用密匙加密了文件,接收者使用同样的密匙解密,获取信息。常见的对称加密算法有:des/aes/3des.

对称加密算法的特点有:速度快,加密前后文件大小变化不大,但是密匙的保管是个大问题,因为消息发送方和接收方任意一方的密匙丢失,都会导致信息传输变得不安全。

  • 非对称加密
    与对称加密相对的是非对称加密,非对称加密的核心思想是使用一对相对的密匙,分为公匙和私匙,私匙自己安全保存,而将公匙公开。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密;如果用私钥对数据进行加密,那么只有用对应的公钥才能解密。发送数据前只需要使用接收方的公匙加密就行了。常见的非对称加密算法有RSA/DSA:

非对称加密虽然没有密匙保存问题,但其计算量大,加密速度很慢,有时候我们还需要对大块数据进行分块加密。

PHP的openssl扩展

openssl扩展使用openssl加密扩展包,封装了多个用于加密解密相关的PHP函数,极大地方便了对数据的加密解密。

非对称加密(RSA为例)

生成RSA私钥,可以指定长度,单位bit

1
openssl genrsa -out yangzie_private.pem 1024

生成对应的公钥

1
openssl rsa -pubout -in yangzie_private.pem -out yangzie_public.pem

你可以把这里生成的公钥拷贝到其他的服务端, 用户加密或者解密数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$private_key_path = storage_path('yangzie_private.pem');
$public_key_path = storage_path('yangzie_public.pem');
$private_key = file_get_contents($private_key_path);
$public_key = file_get_contents($public_key_path);

//判断私钥是否是可用的
$pi_key = openssl_pkey_get_private($private_key);

//判断公钥是否是可用的
$pu_key = openssl_pkey_get_public($public_key);

//原始数据
$data = "hello";

// 加密后的数据
$encrypted = "";

// 解密后的数据
$decrypted = "";

//私钥加密,也可使用openssl_public_encrypt公钥加密,然后使用openssl_private_decrypt解密,加密后数据在$encrypted
openssl_private_encrypt($data, $encrypted, $pi_key);

//加密后的内容通常含有特殊字符,需要编码转换下,在网络间通过url传输时要注意base64编码是否是url安全的
$encrypted = base64_encode($encrypted);

dump($encrypted);

//私钥加密的内容通过公钥可解密出来,公钥加密的可用私钥解密。不能混淆
openssl_public_decrypt(base64_decode($encrypted),$decrypted,$pu_key);

dump($decrypted);

或者

1
2
3
4
5
6
//私钥加密
openssl_private_encrypt($data,$encrypted,$pi_key);
$encrypted = base64_encode($encrypted);
//公钥解密
openssl_public_decrypt(base64_decode($encrypted),$decrypted,$pu_key);
echo $decrypted; //hello

使用PHP自己也可生成一对公私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$config = array(
"digest_alg" => "sha512",
"private_key_bits" => 4096,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
);
// 创建公私钥
$res = openssl_pkey_new($config);
// 获得私钥 $privKey
openssl_pkey_export($res, $privKey);
// 获得公钥 $pubKey
$pubKey = openssl_pkey_get_details($res);
$pubKey = $pubKey["key"];
$data = 'hello';
// 私钥加密
openssl_private_encrypt($data, $encrypted ,$privKey);
// 公钥解密
openssl_public_decrypt($encrypted, $decrypted, $pubKey);
echo $decrypted;

非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。如果既想有很快的加密速度又想保证数据比对称加密更加安全,可对数据进行对称加密,对秘钥做非对称加密,因为一般秘钥的长度会小于数据的长度。

对称加密(AES为例)

AES加密时需要统一四个参数:

  • 密钥长度 (Key Size)
  • 加密模式 (Cipher Mode)
  • 填充方式 (Padding)
  • 初始向量 (Initialization Vector)

由于前后端开发所使用的语言不统一,导致经常出现前后端之间互相不能解密的情况出现,其实,无论什么语言系统,AES的算法总是相同的,导致结果不一致的原因在于上述的四个参数不一致,下面就来了解一下这四个参数的含义

  • 密钥长度
    AES算法下,key的长度有三种:128、192、256 bits,三种不同密钥长度就需要我们传入的key传入不同长度的字符串,例如我们选择AES-128,那我们定的key需要是长度为16的字符串
  • 加密模式
    AES属于块加密,块加密中有CBC、ECB、CTR、OFB、CFB等几种工作模式,为了保持前后端统一,我们选择ECB模式
  • 填充方式
    由于块加密只能对特定长度的数据块进行加密,因此CBC、ECB模式需要在最后一数据块加密前进行数据填充
  • 初始向量
    使用除ECB以外的其他加密模式均需要传入一个初始向量,其大小与Block Size相等

参考Laravel中的加密类vendor\laravel\framework\src\Illuminate\Encryption\Encrypter.php

1
2
3
4
5
6
7
8
9
10
// 加密
$name = 'yangguoqi';
$secret = encrypt($name);

$a = 'eyJpdiI6Im42Z3IzeVNmTDNtXC9nT0NvaGV0NjNRPT0iLCJ2YWx1ZSI6IjhlTGRGYyt6aWhubjREM1BTM0EybExpdlhuMU1FTFwvSUhzUTVnNHRwekJNPSIsIm1hYyI6IjlmZTY3MDczMDg0NzJiMDUwNTRiMGNiYWM3YTAzYzQ4Y2EwOTA4MjJkMDM2OWEyZmVhODcwODUxZWNjMDRkYWEifQ==';
$b = 'eyJpdiI6InJcL1IwcklzbFcyTUZsSVhNS1k3RXFBPT0iLCJ2YWx1ZSI6IlwvV1dCcWdXS21hbERmWnFPWDVYa1hlN0RadkJlQUt4Nkh4bzczS2JuOEFRPSIsIm1hYyI6IjhhN2E0OWE2ZTc3ZTU4YzQzOGYzYmUzZDA3NWYwNTVlOTllNzQ1ZWY5YTE0NzE4NTUzM2E2MDY4ZDVkMzNhOTYifQ==';

// 解密
dump(decrypt($a));
dump(decrypt($b));

题外话

如何限制放在公网的服务器只有局域网内部的人才可以使用或者登录?

一开始想到的就是针对IP的限制, 但是由于公司使用网络的出口 IP 并不固定, 而且 IP 太多, 一个个设置太过麻烦, 然后在想想是否可以通过这样的方式, 比如进入系统的第一步就是登录, 我们就在登录这做一些手脚, 登录的时候在JS访问一个放置在局域网内的脚本, 可以返回一些加密过后约定的内容, 连同登录表单一起提交到系统后台验证, 因为外网的人无法访问到这个脚本, 既然也无法拿到我们事先约定好的加密数据, 自然无法登录.
JS脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script>
$(function () {
$.ajax({
url: 'http://192.168.1.6/zq_check_login/auth.php',
dataType: 'jsonp', jsonp: 'hash',
data: {get: 1}
}).done(function ( data ) {
if (data.code === 200) {
var token = $("<input/>").attr({
name: 'access_token',
type: 'hidden'
}).val(data.token);

var ip = $("<input/>").attr({
name: 'access_private_ip',
type: 'hidden'
}).val(data.ip);

$("form").prepend(token, ip);

$("button[type='submit']").attr('disabled', false);
}
});
});
</script>

登录验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$publicKey = file_get_contents(storage_path('app/public.pem'));
$key = openssl_pkey_get_public($publicKey);
$encode = base64_decode($request['access_token']);
$result = openssl_public_decrypt($encode, $decrypted, $key);
if (!$result) {
return flash_message('身份验证失败!');
}

$arr = json_decode($decrypted, true);
$rt = substr(now()->toDateTimeString(), 0, 13);

if (!starts_with($arr['date'], $rt)) {
return flash_message('身份验证失败,请重试。');
}

if (!starts_with($arr['ip'], '192.168') && !starts_with($arr['ip'], '127.')) {
return flash_message('身份验证失败,请重试!');
}

只要从现在开始努力,最坏不过是大器晚成

当 Swoole 启动时, 一共会建立 2 + n + m 个进程,其 n 为 Worker 数, m 为 TaskWorker 数,2 为一个 Master 进程和一个 Manager 进程

Swoole框架模型

Swoole Server 中有以下几种角色,分别是:Master(Process)、Reactor(Thread)、Manager(Process)、Worker(Process) 和 TaskWoker(Process);

Master(Process)

Master 是 Swoole 的主要 Process,当我们启动 Swoole 时,当下的 Process 就会成为 Master Process 并负责建立 Main Reactor、建立和管理 Reactor、建立 Manager 并开始接受客户端请求等工作。

首先 Master 会透过 fork 函数建立一个 Manager,接着建立 Reactor,当全部建立完成后会呼叫 onStart 的 callback function,此时可以在这个 callback function 中对 Master 做一些处理或修改。如:将 Master 重新命名,保存 Master 的 PID 档桉等。

这裡要特别注意,因为 Master 不应该存在任何商业逻辑,因此 Swoole 底层会禁止你在此处做一些行为,如:发送请求、呼叫 Swoole 的异步 API 等等。

Reactor(Thread)

Reactor 以 Multi-Thread 的方式执行,Reactor 底层由 epoll 函数来实现(在Mac系统是透过 Kqueue),用于实际监听和处理来自客户端的 Connect 请求,并完全是透过异步、非阻塞的模式来运作。

Manager(Process)

Swoole 中 Worker/Task worker 都是由 Manager 所 fork 并管理的。当 Manager 被建立时,会呼叫 onManagerStart 的 callback function 通知上层的应用。

当底下的某个 Worker 意外结束执行时,Manager 会负责回收的工作,并同时建立新的 Worker 补足固定的数量维持 Swoole 的正常运作。

当 Manager 退出时,会呼叫 onManagerStop 的 callback function,可以利用此时进行一些回收逻辑。

Worker(Process)

Worker 以 Multi-Process 的方式执行,是 Swoole 中执行大部分逻辑的地方,因此在 Worker 中所对应的 callback function 数量也是最多的。

当 Worker 启动后会呼叫 onWorkerStart 的 callback function,在这裡我们就能够使用全部 Swoole 所提供的 API 了。

当上层的 Reactor 收到客户端的请求后,就会将数据打包发送给 Worker,并在相对应的 callback function 中(如:onReceive、onRequest、onMessage等)处理这些资料,并可以将处理结果回传给 Reactor,再回传至客户端中。如果 Worker 正常退出,会呼叫 onWorkerStop 的 callback function,若是处理数据过程中出现严重错误或者 Worker 请求达到处理上限时,则会呼叫 onWorkerError 的 callback function 并结束该 Worker。

Task Worker(Process)

Task Worker 一样以 Multi-Process 的方式执行,只接受由 Worker 中透过 swoole_server->task/taskwait 方法指派过来的任务,并将结果回传给 Worker。

Process 间的通讯

在 Swoole 中,Process 间的通讯可以分为以下几三种状况:

  • Master <=> Worker
  • Worker <=> Worker
  • Worker <=> Task Worker

前两种情形,在 Swoole 底层统一使用 Unix Socket 进行通讯,这些 socket 也都归併到各自 Process 的 Reactor 中进行管理。而第三种情况,除了预设的 Unix Socket 外,还可以使用由系统提供的 Message Queue 来实作。

Reactor,Worker,Task的关系

一个通俗的比喻,假设Server就是一个工厂,那Reactor就是销售,接受客户订单。而Worker就是工人,当销售接到订单后,Worker去工作生产出客户要的东西。而TaskWorker可以理解为行政人员,可以帮助Worker干些杂事,让Worker专心工作。

https://wiki.swoole.com/wiki/page/163.html
https://blog.albert-chen.com/swoole-basic-concepts/

谢谢你啊

在一般编程中,我们要扩展一个基础类,我们需要进行继承才能扩充。然而Laravel利用PHP的特性,编写了一套叫做Macroable的Traits,这样,凡是使用Macroable的类,都是可以使用这个方法扩充的。

邮件发送动态设置

1
2
3
4
5
6
7
8
\Mail::macro('setConfig', function (string $key, string $domain) {

$transport = $this->getSwiftMailer()->getTransport();
$transport->setKey($key);
$transport->setDomain($domain);

return $this;
});

使用

1
2
3
4
5
6
7
8
9
10
11
public function build()
{

$to = 'lnmput@gmail.com';

$mailgunConfig = 'erp.template.'.$mailTplDir.'.mailgun.';

\Mail::setConfig(config($mailgunConfig.'secret'), config($mailgunConfig.'domain'));

return $this->to($to)->subject($subject)->from($mailFrom, $mailFromName)->view($view);
}

集合转小写

1
2
3
4
5
Collection::macro('uppercase', function () {
return collect($this->items)->map(function ($item) {
return strtoupper($item);
});
});

使用

1
collect(["hello", "world"])->uppercase();

自定义响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
\Response::macro('xml', function(array $vars, $status = 200, array $header = [], $xml = null)
{
if (is_null($xml)) {
$xml = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><response/>');
}
foreach ($vars as $key => $value) {
if (is_array($value)) {
\Response::xml($value, $status, $header, $xml->addChild($key));
} else {
$xml->addChild($key, $value);
}
}
if (empty($header)) {
$header['Content-Type'] = 'application/xml';
}
return \Response::make($xml->asXML(), $status, $header);
});

使用

1
2
3
4
5
6
$data = ['status' => 'OK', 'data' => [
'name' => 'yangguoqi',
'age' => 27
]];

return Response::xml($data);

统计字符串中单词数量

1
2
3
4
5
6
use Illuminate\Support\Str;

Str::macro('countWords', function($value)
{
return str_word_count($value);
});

使用

1
$value = Str::countWords('This is test'); //3

字符串CSV转array

1
2
3
4
Str::macro('csvToArray', function ($string)
{
return array_filter(array_map('trim', str_getcsv($string)));
});

使用

1
2
3
$string = "some, crazy, , , , mixed, bag, of , comma-separated values, , ,";
$array = \Illuminate\Support\Str::csvToArray($string);
dd($array);

模型是否分页

1
2
3
4
5
6
7
Builder::macro('paginateIf', function($callback = true) {
if($callback) {
return $this->paginate();
}

return $this->get();
});

使用

1
$data = Model::oldest()->paginateIf($request->has('page'));

哪些类可以使用marco

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Illuminate\Database\Query\Builder
Illuminate\Database\Eloquent\Builder
Illuminate\Database\Eloquent\Relations\Relation
Illuminate\Http\Request
Illuminate\Http\RedirectResponse
Illuminate\Http\UploadedFile
Illuminate\Routing\Router
Illuminate\Routing\ResponseFactory
Illuminate\Routing\UrlGenerator
Illuminate\Support\Arr
Illuminate\Support\Str
Illuminate\Support\Collection
Illuminate\Cache\Repository
Illuminate\Console\Scheduling\Event
Illuminate\Filesystem\Filesystem
Illuminate\Foundation\Testing\TestResponse
Illuminate\Translation\Translator
Illuminate\Validation\Rule
Mail

等等,使用了Marcoable的Traits,如果是自己编写的类,使用了Marcoable,也可以这样扩充使用(写Laravel开源库的时候)

https://laravel-tricks.com/tricks/responsexml-macro

你只见到这么多人说他不好时,却看到他有出来说別人一句吗?

Laravel 5.2 provides some nice additions to the framework. One handy feature that I don’t see listed in the release notes is that Collection now is macroable. Using it’s macro function you can easily extend Illuminate\Support\Collection with your own custom functions.

Take a look at this piece of code to uppercase every string in a collection.

1
2
3
$uppercaseWords = collect(['code', 'ferengi'])->map(function($word)  {
return strtoupper($word);
});

That’s good code, but image you need to uppercase a lot of collections. Typing the same closure will get very tiresome. Let’s improve this with a macro.

1
2
3
4
5
6
7
8
9
use Illuminate\Support\Collection;

Collection::macro('uppercase', function() {

return collect($this->items)->map(function($word) {
return strtoupper($word);
});

});

You could create a service provider to load up these macro’s. Now that the macro is defined let’s uppercase collections like there’s no tomorrow:

1
2
3
$uppercaseWords = collect(['code', 'ferengi'])->uppercase();
$moreUppercaseWords = collect(['love', 'the', 'facade'])->uppercase();
$evenMoreUppercaseWords = collect(['activerecord', 'forever'])->uppercase();

You could be thinking “Why should I use a macro? I can easily to this with a regular function.”. Consider this piece of code.

1
2
3
4
5
function uppercase($collection) {
...
}

$uppercaseWords = uppercase(collect(['halo','five']));

It works, but you have to encapsulate the collection with your function. The last executed function is put first, which is confusing. With macro’s you can still chain functions and greatly improve readability.

1
2
3
4
5
6
7
8
9
//lots of functions
function4(function3(function2(function1(collect(['jack','cheats'])))));

//lots of macros
collect(['i', 'want', 'to', 'live', 'in', 'a', 'desert'])
->function1()
->function2()
->function3()
->function4();

Sure, the examples use in this post were a bit contrived, but I hope you see that collection macro’s can be very handy.

https://murze.be/using-collection-macros-in-laravel

我现在这样子,你看,还有机会吗?
最近在做项目中遇到了锁表的问题, 页面迟迟不能响应, 直到超时, 一开始想到到是慢查询, 所以叫运维去查看, 发现并没有慢查询的日志, 当时并没有注意到会是MySql表锁的问题, 特此纪念;

我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式。同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力。所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在。

一次封锁or两段锁?

因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。
数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)

  • 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
  • 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的。

事务的四种隔离级别

在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库锁,也是为了构建这些隔离级别存在的。

1
2
3
4
5
隔离级别	                        脏读(Dirty Read)	不可重复读(NonRepeatable Read)	幻读(Phantom Read)
未提交读(Read uncommitted) 可能 可能 可能
已提交读(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 可能
可串行化(Serializable ) 不可能 不可能 不可能
  • 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
  • 提交读(RC):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
  • 可重复读(RR):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读
  • 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

MySQL中锁的种类

MySQL中锁的种类很多,有常见的表锁和行锁,也有新加入的Metadata Lock等等,表锁是对一整张表加锁,虽然可分为读锁和写锁,但毕竟是锁住整张表,会导致并发能力下降,一般是做ddl处理时使用。

行锁则是锁住数据行,这种加锁方法比较复杂,但是由于只锁住有限的数据,对于其它数据不加限制,所以并发能力强,MySQL一般都是用行锁来处理并发事务。这里主要讨论的也就是行锁。

Read Committed(读取提交内容)

在RC级别中,数据的读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MySQL> show create table class_teacher \G\
Table: class_teacher
Create Table: CREATE TABLE `class_teacher` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`class_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`teacher_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_teacher_id` (`teacher_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.02 sec)
MySQL> select * from class_teacher;
+----+--------------+------------+
| id | class_name | teacher_id |
+----+--------------+------------+
| 1 | 初三一班 | 1 |
| 3 | 初二一班 | 2 |
| 4 | 初二二班 | 2 |
+----+--------------+------------+

由于MySQL的InnoDB默认是使用的RR级别,所以我们先要将该session开启成RC级别,并且设置binlog的模式

1
2
SET session transaction isolation level read committed;
SET SESSION binlog_format = 'ROW';(或者是MIXED)
事务A 事务B
begin; begin;
update class_teacher set class_name=’初三二班’ where teacher_id=1; update class_teacher set class_name=’初三三班’ where teacher_id=1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
commit;

为了防止并发过程中的修改冲突,事务A中MySQL给teacher_id=1的数据行加锁,并一直不commit(释放锁),那么事务B也就一直拿不到该行锁,wait直到超时。
这时我们要注意到,teacher_id是有索引的,如果是没有索引的class_name呢?update class_teacher set teacher_id=3 where class_name = ‘初三一班’;
那么MySQL会给整张表的所有数据行的加行锁。这里听起来有点不可思议,但是当sql运行的过程中,MySQL并不知道哪些数据行是 class_name = ‘初三一班’的(没有索引嘛),如果一个条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由MySQL Server层进行过滤。

但在实际使用过程当中,MySQL做了一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见即使是MySQL,为了效率也是会违反规范的。(参见《高性能MySQL》中文第三版p181)

这种情况同样适用于MySQL的默认隔离级别RR。所以对一个数据量很大的表做批量修改的时候,如果无法使用相应的索引,MySQL Server过滤数据的的时候特别慢,就会出现虽然没有修改某些行的数据,但是它们还是被锁住了的现象。

Repeatable Read(可重读)

这是MySQL中InnoDB默认的隔离级别

RC(不可重读)模式下的展现:

事务B修改id=1的数据提交之后,事务A同样的查询,后一次和前一次的结果不一样,这就是不可重读(重新读取产生的结果不一样)。这就很可能带来一些问题,那么我们来看看在RR级别中MySQL的表现:

我们注意到,当teacher_id=1时,事务A先做了一次读取,事务B中间修改了id=1的数据,并commit之后,事务A第二次读到的数据和第一次完全相同。所以说它是可重读的。

不可重复读和幻读的区别

不可重复读重点在于update和delete,而幻读的重点在于insert。

在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。

悲观锁和乐观锁

悲观锁

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

乐观锁

乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

https://tech.meituan.com/innodb-lock.html

像我这样的人...

composer.lock文件是否需要版本控制器

  • 什么是 composer.lock 文件?
    composer.lock 文件是当你第一次使用 composer install 或者 执行 composer update 后生成的文件, 此文件里定义了当前项目的代码依赖, 还有最重要的, 这些代码依赖的对应的版本.

  • composer.lock 文件作用是什么?
    默认情况下, 当执行 composer install 的时候, Composer 会检查当前项目是否有 composer.lock 文件, 如果有的话, 就会按照此文件去下载代码依赖和其指定的版本.

  • 好处
    团队开发的时, clone 下代码后, 使用 composer install 可以确保大家使用的依赖包都是同一个版本的, 避免没必要的混乱;
    在一个现有的项目上开发的时候, 执行 composer update 后, 偶尔会发现刚刚更新了某个代码包把程序整挂了, 这个时候, 如果 composer.lock 是加入版本控制器的话, 直接一个 git diff 命令, 就可以查看到这次更新了那个包, 快速定位到问题的所在;
    在线上部署的时候, 可以确保线上生成环境下使用所有代码是和开发时候使用的一致, 因为 composer.lock 会确保你在执行 composer install 命令后, 按照文件里面指定的版本去下载代码依赖包;

composer install与composer update的区别

composer install

install 命令从当前目录读取 composer.json 文件,处理了依赖关系,并把其安装到 vendor 目录下。

1
composer install

如果当前目录下存在 composer.lock 文件,它会从此文件读取依赖版本,而不是根据 composer.json文件去获取依赖。这确保了该库的每个使用者都能得到相同的依赖版本。
如果没有composer.lock 文件,composer 将在处理完依赖关系后创建它。

composer update

为了获取依赖的最新版本,并且升级 composer.lock 文件,你应该使用 update 命令。

1
composer update

这将解决项目的所有依赖,并将确切的版本号写入 composer.lock