更多官方文档 https://kubernetes.io/zh/docs/tasks/administer-cluster/network-policy-provider/calico-network-policy/ https://www.kubernetes.org.cn/cronjob使用kubeadm 部署k8s集群ps: k8s 不能直接管理容器 最小单位为pod 需要借助docker前期准备1. 三台虚拟机 分别设置主机名 hostnamectl set-hostname master1 、worker1 、worker22. 网络修改 vi /etc/sysconfig/network-scripts/ifcfg-ens33 BOOTPROTO none/static IPADDR="192.168.199.128" 当前虚拟机ip PREFIX="24" GATWAY="192.168.199.2" 一般为当前网段的.2 DNS1="119.29.29.29" 重启网卡 systemctl restart network 3. 安全配置关闭firewalld selinux systemctl stop firewalld systemctl disable firewalld sed -ri 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config 4. 时间同步 yum -y install ntpdate ntpdate time1.aliyun.com6. kubeadm安装需要关闭swap 分区 vim /etc/fstab free -m 查看 如果有swap分区 需要关闭7. 添加网桥过滤 cat /etc/sysctl.d/k8s.cof net.bridge.bridge-nf-call-ip6tables =1 net.bridge.bridge-nf-call-iptables =1 net.ipv4.ip_forward = 1 vm.swappiness = 0 modprobe br_netfilter sysctl -p /etc/sysctl.d/k8s.conf 8. 安装 ipvs yum -y install ipset ipvsadm cat > /etc/sysconfig/modules/ipvs.modules <<EOF #!/bin/bash modprobe -- ip_vs modprobe -- ip_vs_rr modprobe -- ip_vs_wrr modprobe -- ip_vs_sh modprobe -- nf_conntrack_ipv4 EOF 权限改为 755 chmod 755 /etc/sysconfig/modules/ipvs.modules && bash /etc/sysconfig/modules/ipvs.modules && lsmod |grep -e ip_vs -e nf_conntrack_ipv4 9. 安装docker wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/centos/docker-ce.repo yum -y install --setopt=obsoletes=0 docker-ce-18.06.3.ce-3.el7 使用18.06 高版本配置可能需要修改 systemctl enable docker && systemctl start docker10. 修改资源配置vim /etc/docker/daemon.json { "exec-opts":["native.cgroupdriver=systemd"], "registry-mirrors":["https://docker.mirrors.ustc.edu.cn"] } 11. 集群软件安装yum -y install --setopt=obsoletes=0 kubeadm-1.17.2-0 kubelet-1.17.2-0 kubectl-1.17.2-0 kubeadm 初始化集群 kubelet 接受api-server 指令 管理pod kubectl 集群命令管理工具 12. 修改配置 cat <<EOF > /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64 enabled=1 gpgcheck=0 repo_gpgcheck=0 gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg EOF保持cgroupdriver docker与kubelet 一致 vim /etc/sysconfig/kublet KUBELET_EXTRA_ARGS="--cgroup-driver=systemd" systemctl enable kubelet 这里是我设置的代理 k8s 的镜像需要翻墙才能pullmkdir -p /etc/systemd/system/docker.service.d && cat > /etc/systemd/system/docker.service.d/https-proxy.conf << EOF [Service] Environment="HTTP_PROXY=192.168.1.5:9999/" "HTTPS_PROXY=192.168.1.5:9999/" "NO_PROXY=localhost,127.0.0.1,docker-registry.example.com," EOF systemctl daemon-reload systemctl restart docker systemctl show --property=Environment docker 13. 保存镜像传输到其他服务器worker 服务器只需要这两个镜像 docker save -o kube-proxy.tar k8s.gcr.io/kube-proxy docker save -o pause .......14. k8s 集群初始化在master 节点 kubeadm init --kubernetes-version=v1.17.3 --pod-network-cidr=172.16.0.0/16 --apiserver-advertise-address=主机ip192.168.137.132 docker pull rancher/calico-cni:v3.8.1; docker pull rancher/calico-node:v3.8.1; docker pull calico/kube-controllers:v3.13.0-0-g15e951d; docker pull calico/pod2daemon-flexvol:release-v3.13; 下载 calico.yaml到~目录 我是在官网下载做了下面的修改 9 - name: CLUSTER_TYPE 620 value: "k8s,bgp" 621 # Auto-detect the BGP IP address. 622 - name: IP 623 value: "autodetect" 624 - name: IP_AUTODETECTION_METHOD 625 value: "interface=ens.*" - name: CALICO_IPV4POOL_CIDR 639 value: "172.16.0.0/16" 15. 初始化之后需要做什么 在初始化结束会有提示Your Kubernetes control-plane has initialized successfully! 创建文件夹 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config Then you can join any number of worker nodes by running the following on each as root: 在worker节点中执行下面内容 加入集群 kubeadm join 192.168.137.132:6443 --token hbb5ve.v0ho11tpni2gjw4i \ --discovery-token-ca-cert-hash sha256:028e5215303b520ec0fa1d32e1812d238f148eb95440489dd48ffe4d4a6000f0 查看集群节点状态 kebectl get nodes查看集群健康状态 kubecel get cs 或者 kubectl cluster-info工作状态 kubectl get pods --namespace kube-system集群工具 kubectl 有 .kube/config 才可以使用kubectl 可以将这个文件复制到 worker机器 Namespace 命名空间介绍 多用户 实现资源隔离属于逻辑隔离 属于管理边界 不属于网络边界(不需要部署多套网络构建) 可以针对每个 namespace做资源配额查看 NameSpace kubectl get namespace default 默认 kube-node-lease 集群节点租约状态 kube-public 所有用户都可访问 包括未认证用户 kube-system kubernetes集群在使用创建NameSpace 命令创建kubectl create namespace test yml资源清单创建 kubectl apply -f create-ns.ymlapiVersion: v1 kind: Namespace metadata: name: demons2 删除NameSpacekubectl delete ns test1 kubectl deltete -f **.yml最小管理单元podpod 是容器的封装 查看 kubectl get pods --ns default 创建 kubectl apply -f **.ymlapiVersion: v1 kind: Pod metadata: name: pod1 spec: containers: - name: nginx-pod image: nginx:latest ports: - name:nginxport containerPort:80kubectl get pods -o wide 生产环境最好不要直接访问pod 删除pod kubectl delete pods pods1 yml文件删除 kubectl delete -f **.yml k8s 核心概念controller 控制器loop 方式监控pod 状态 如果发生删除会重新拉起控制器作用对应用`运行的资源对象监控 出现问题 重新拉起 达到期望状态 控制器分类 官网介绍更全可以自定义 常见控制器 Deployment 声明式 更新控制器 发布无状态应用 ReplicaSet 副本集控制器 对Pod 进行规模扩大杰少 StatefulSet 有状态副本集 mysql 用于发布有状态应用 DaemonSet 每一个Node 上运行一个副本 用于发布监控或日志收集 Job 运行一次性作业任务 CronJob 运行周期性任务 Deployment 控制器介绍、命令创建 默认 Deployment kubectl run nginx-app --image=nginx:latest -image-pull-policy-IfNotPresent --replicas=2 //2个pod运行 kubectl get deployment.apps kubectl get replicaset.apps ------------------------ apiVersion: apps/v1 s kind: Deployment metadata: name: nginx-app2 spec: replicas: 1 #副本数量 selector: #标签选择器 与pod 进行关联 matchLabels: app: nginx template: #pod 模板 metadata: labels: app: nginx spec: containers: - name: nginx-app2container image: nginx:latest imagePullPolicy: IfNotPresent ports: - name: nginxapp2 containerPort: 80 删除应用 带有控制器的Pod 不能直接删除 会自动拉起kubectl get deployment.apps kubectl delete deployment.apps **k8s 核心概念 service 服务 不是实体服务 是一条iptables 或者 ipvs 的转发规则 通过service为pod客户端提供访问pod 的方法=客户端访问pod的入口 service 同pod 标签与pod 关联 service 类型-- ClusterIP 默认分配一个集群内部可以访问的虚拟ip -- NodePort 在每一个Node 上分配一个端口作为外部访问 -- oadBalancer 工作在特定的 云服务上 aws openstart -- ExternalName 表示把几圈外部服务引入到集群内部来实现进群内部pod 与集群外部的服务进行通信 - service 常用参数 port 访问service使用的端口 targetPort pod 中容器端口 NodePort 通过 Node 实现外网访问k8s 内service (30000-32767)创建 service命令行创建 默认创建为ClusterIp 先创建应用 kubectl run nginx-app --image=nginx:latest --image-pull-policy=IfNotPresent --replicas=1 创建service kubectl expose deployment.apps nginx-app --type=ClusterIP --target-port=80 --port=80 yml 创建service--- apiVersion: apps/v1 kind: Deployment metadata: name: nginx-app2 spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginapp2 image: nginx:latest imagePullPolicy: IfNotPresent ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: nginx-app2 spec: type: ClusterIP selector: app: nginx ports: - protocol: TCP port: 80 targetPort: 80 查看kubectl get servicekubectl get endpoints创建 NodePort 类型的service 只需要修改type即可service 自带负载均衡 均匀访问多个 pod
防火墙，其实说白了讲，就是用于实现Linux下访问控制的功能的，它分为硬件的或者软件的防火墙两种。无论是在哪个网络中，防火墙工作的地方一定是在网络的边缘。而我们的任务就是需要去定义到底防火墙如何工作，这就是防火墙的策略，规则，以达到让它对出入网络的IP、数据进行检测。目前市面上比较常见的有3、4层的防火墙，叫网络层的防火墙，还有7层的防火墙，其实是代理层的网关。对于TCP/IP的七层模型来讲，我们知道第三层是网络层，三层的防火墙会在这层对源地址和目标地址进行检测。但是对于七层的防火墙，不管你源端口或者目标端口，源地址或者目标地址是什么，都将对你所有的东西进行检查。所以，对于设计原理来讲，七层防火墙更加安全，但是这却带来了效率更低。所以市面上通常的防火墙方案，都是两者结合的。而又由于我们都需要从防火墙所控制的这个口来访问，所以防火墙的工作效率就成了用户能够访问数据多少的一个最重要的控制，配置的不好甚至有可能成为流量的瓶颈本次介绍iptables防火墙iptables防火墙说明及工作原理iptables防火墙包含四个表和五个链，分别为四个表分别为：Filter表（真正的防火墙功能）Nat表（负责数据包的改写）Mangle表（路由标记）raw表（禁止数据包的连接跟踪，用于提高性能）五个链分别为：INPUT(进入防火墙)OUTPUT(离开防火墙)FORWARD(流经防火墙)PREROUTING(数据包到达防火墙之前进行路由判断)POSTROUTING(数据包离开防火墙时进行的路由判断)3.表和链的包含情况说明Filter表包含: INPUT、OUTPUT、FORWARD;Nat表包含: OUTPUT、PREROUTING、POSTROUTING；Mangle表包含： INPUT、OUTPUT、FORWARD、PREROUTING、POSTROUTING；Raw表包含: OUTPUT、PREROUTING；4.iptables防火墙的工作原理说明1、iptables防火墙是一层层过滤的，实际是按照配置规则的顺序从上到下，从前到后进行过滤的。2、如果匹配上规则，即明确表明是阻止还是通过，此时数据包就不在向下匹配新规则了。3、如果所有规则中没有明确表示是阻止还是通过这个数据包，也就是没有匹配上规则，向下进行匹配，直到匹配到默认规 则到明确阻止还是通过。4、防火墙的默认规则是对应链的所有规则执行完才会执行的。注：防火墙规则执行顺序默认是从前到后，（从上到下）依次执行，遇到匹配的规则就不在继续向下检查，只有遇到不匹配的规则才会继续向下进行匹配Iptables防火墙使用及规则配置iptables命令常用参数很多参数请通过man iptables或iptables --help来查看-n：以数字的方式显示ip，它会将ip直接显示出来，如果不加-n，则会将ip反向解析成主机名。-L（大写）列表 例：iptbles -nL 查看iptables规则--line-number显示规则的序号 iptables -nL --line-number （可以根据序号删除规则）-D 删除规则 （查到规则序号后、指定链、指定序号删掉） 例： iptables -D INPUT 1 （指顶删除INPUT链的第一条规则、默认是FILTER表、如果是其他的表需要-t指定表）-F（大写）清除所有规则，不会处理默认的规则-X（大写）删除用户自定义的链-Z（大写）清除链的计数器-A （大写）添加规则到指定链的结尾-I (大写) 添加规则到指定链的开头-P （大写）设置默认策略的（设定默认门是关着的还是开着的）-t 指定表（默认是filter表） 例：iptables -t filter -A INPUT -p tcp --dport 443 -j DROP #指定在filter表的input链的tcp协议443端口的请求丢弃-p 匹配指定协议 （all（所有） 、tcp、udp、icmp（ping等）、默认all）--dport 目的端口（对本地的端口做什么）--sport 源端口（从那个端口连接过来的）-j 处理的行为 {DROP(丢弃)、ACCEPT(接受)、REJECT(拒绝)、ACCEPT(接受)-s 指定源ip地址-d 目的地址-i 匹配进入的网卡接口eth0或者eth1进行控制-o 匹配出去的网卡借口配置一个生产中只开启443端口的防火墙规则说明：这里配置的防火墙规则只开启443端口（https），1.首先清除防火墙的默认规则iptables -Fiptables -Xiptables -Z2.允许自己的网段连接服务器iptables -A INPUT -p tcp -s 10.0.0.0/24 -j ACCEPT允许自己的回环接口进出：iptables -A INPUT -i lo -j ACCEPTiptables -A OUTPUT -o lo -j ACCEPT3.设置默认规则：iptables -P INPUT DROP #忽略进入的请求iptables -P FORWARD DROP #忽略要通过iptables -P OUTPUT ACCEPT #允许出去的请求4.配置规则iptables -A INPUT -s 124.43.62.96/27 -p all -j ACCEPT #允许办公室的ip段连接iptables -A INPUT -s 192.168.1.0/24 -p all -j ACCEPT #允许其他机房的网段链接iptables -A INPUT -p tcp --dport 443 -j ACCEPT #允许业务端口对外提供链接iptables -A INPUT -p icmp -m icmp --icmp-type any -j ACCEPT #允许所有人pingiptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT #允许关联包通过iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT至此防火墙就配完了。以上命令是在命令行执行的没有永久生效，重启主机就失效了：永久保存的方法：services iptables save就保存到/etc/sysconfig/iptables中了以后维护iptables就可以通过编辑/etc/sysconfig/iptables来更改规则了。CentOS 7自带的防火墙是firewalld，需要下载iptables：yum install -y iptables-services
发现问题据说mariadb和mysql 90% 相似度,本地使用docker 起了个容器 挂载数据到本地的时候居然报错了[ERROR] InnoDB: The Auto-extending innodb_system data file './ibdata1' is of a different size 0 pages than specified in the .cnf file: initial 768 pages, max 0 (relevant if non-zero) pages!查询问题和 mysql 一样的配置为什么会报错的,一脸懵逼??? ./ibdata1 查询了各大资源后发现可能是 Docker for windows 的共享文件系统或MariaDB本身中的bug!!!解决问题最后经过苦苦测试 终于找到了一个可行的解决方案 下面解释一下关键配置ibdata1是InnoDB的共有表空间，默认情况下会把表空间存放在一个文件ibdata1中innodb_flush_method 则确定日志及数据文件如何write、flushinnodb_flush_method可以取如下值：fdatasync, O_DSYNC, O_DIRECT 实验选项littlesync和nosyncinnodb_use_native_aio 该选项仅适用于Linux系统，默认情况下处于启用状态。在其他类Unix系统上,InnoDB仅使用同步I/O. - 从历史上看,InnoDB仅在Windows系统上使用异步I/O. 在Linux上使用异步I/O子系统需要libaio库Binary Log 简称 Binlog 即 二进制日志文件，这个文件记录了mysql所有的 DML 操作。通过 Binlog 日志我们可以做数据恢复，做主主复制和主从复制等等。下面是的我的 docker-compose.yml 和 配置dockerFileFROM mariadb:latest LABEL maintainer="Mahmoud Zalt <mahmoud@zalt.me>" COPY my.cnf /etc/mysql/conf.d/my.cnf CMD ["mysqld"] EXPOSE 3306docker-compose.yml mariadb: build: ./mariadb volumes: - ./mariadb/data:/var/lib/mysql ports: - "3306:3306" environment: - MYSQL_DATABASE=default - MYSQL_USER=default - MYSQL_PASSWORD=default - MYSQL_ROOT_PASSWORD=root networks: - backend command: mysqld --innodb-flush-method=littlesync --innodb-use-native-aio=OFF --log_bin=ONmariadb 配置[mysqld] innodb_flush_method=littlesync innodb_use_native_aio=OFF log_bin=ON
安装包 （win10并开启虚拟化）https://download.docker.com/win/stable/Docker%20for%20Windows%20Installer.exegit 仓库git@github.com:weiyixi/docker_study.git目录说明nginx 站点配置在 ./docker-lnmp/nginx/site (修改后重启nginx容器生效)php-fpm 容器中/var/www/目录挂载本地docker-lnmp的上层目录（.env中 APPLICATION=../）需要加扩展在对应得dockerfile 中修改1.修改配置env-example 复制为 .env //window需要修改 DOCKER_SYNC_STRATEGY=unison //php版本修改默认7.2 PHP_VERSION=722.基本操作首先到docker-lnmp目录下 1.构建 docker-compose build nginx php-fpm redis mysql 2.开启 docker-compose up -d nginx redis mysql 3.关闭 docker-compose down 4.查看运行中容器 docker ps 5.进入容器中 docker exec -it 对应的CONTAINER_ID bash 3.扩展1.已经定义了一批扩展可在 .env中开启 true开启 false 关闭 2.未定义的需要修改dockerfile安装 3.修改后需要重新build 开启才会生效
适用于对docker有一定了解的朋友，小白请先仔细阅读官方文档！作为应用程序，我们通常需要依赖于多种外部服务，比如数据库、缓存服务等等。Docker-compose就是在Docker容器的基础上，提供了统一的容器编排语言，可以让你更轻松的利用Docker构建你的应用环境。docker-compose.yml文件语法语法image 使用的镜像 build 指定Dockerfile构建 command 启动执行命令 links 链接其他容器 ports 端口映射 expose 暴露端口 volumes 挂载路径 volumes_from 从容器挂载 environment 环境变量编写dockerfile(自定义镜像)编写docker-compose.yml(可以不依赖Dockerfile 使用官方镜像)常用命令构建容器 docker-compose up -d 查看容器 docker-compose ps 进入容器 docker exec -it 容器名 /bin/bash 关闭所有 docker-compose downphp-fpm Dockerfile 示例 可以自定义扩展FROM php:7.1-fpm-alpine # apk RUN apk --update add \ autoconf \ build-base \ linux-headers \ libaio-dev \ zlib-dev \ curl \ git \ subversion \ freetype-dev \ libjpeg-turbo-dev \ libmcrypt-dev \ libpng-dev \ libtool \ libbz2 \ bzip2 \ bzip2-dev \ libstdc++ \ libxslt-dev \ openldap-dev \ imagemagick-dev \ make \ unzip \ wget \ libmemcached-dev # PHP Core Extensions RUN docker-php-ext-install \ bcmath \ pdo_mysql \ mysqli \ pcntl \ && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \ && docker-php-ext-install gd # PECL Extensions RUN pecl install redis-4.2.0 \ && pecl install mongodb-1.5.3 \ && pecl install memcached-3.1.3 \ && pecl install swoole-4.3.1 \ && docker-php-ext-enable redis mongodb memcached swoole # Delete RUN apk del build-base \ linux-headers \ libaio-dev \ && rm -rf /var/cache/apk/* \ && mkdir /data \ && chmod -R 777 /data VOLUME /var/www WORKDIR /var/www EXPOSE 9000 CMD ["php-fpm"]docker-compose.yml示例version: "3.0" services: nginx: image: nginx:alpine ports: - 80:80 - 443:443 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf - ./nginx/conf.d:/etc/nginx/conf.d - ../:/var/www php-fpm: build: ./php-fpm ports: - 9000:9000 volumes: - ./php-fpm/php.ini:/usr/local/etc/php/php.ini - ./php-fpm/www.conf:/usr/local/etc/php-fpm.d/www.conf - ../:/var/www mysql: image: mysql:5.7 ports: - 3306:3306 volumes: - ./mysql/data:/var/lib/mysql environment: - MYSQL_ROOT_PASSWORD=123456 redis: image: redis:5.0-alpine ports: - 6379:6379 volumes: - ./redis/data:/data #- ./redis/redis.conf:/usr/local/etc/redis/redis.conf mongo: image: mongo ports: - 27017:27017 volumes: - ./mongo/data:/data/db restart: always environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: 123456
<?php /** * 爬取 国外高校犯罪信息数据 * Class SchoolSpider */ class SchoolSpider { private $schoolName; //学校名检查api private $schoolApi = 'https://ope.ed.gov/campussafety/api/institution/names?filter='; //校区 private $schoolSerachApi = 'https://ope.ed.gov/campussafety/api/institution/search'; //数据 校区 private $dataApi = 'https://ope.ed.gov/campussafety/api/campus/'; //整个学校 private $totalApi = 'https://ope.ed.gov/campussafety/api/institution/'; //校区 private $campisesData; //详情数据 private $dataDetail; //详情数据 public $error; public $result=[]; public function __construct() { //请求时间不限 set_time_limit(0); } public function spiderData($schoolName) { $this->schoolName = $schoolName; $this->result['school_name'] = $this->schoolName; if (!$this->checkName() || !$this->getCampises()) { return ['code' => -1, 'msg' => $this->error]; } //总的数据 $this->formatData($this->campisesData); //各个分校区数据 $this->formatData($this->campisesData[0]['Campuses'],1); return ['code' => 0, 'msg' => $this->error,'data'=>$this->result]; } /** * @name 数据解析 * @param $campisesData 数据 * @param $isCampise 是否是分校区数据 */ private function formatData($campisesData,$isCampise=false){ foreach ($campisesData as $campise) { if($isCampise){ //校区名 $res = $this->getDetailCampuse($campise['UnitID']); }else{ $res = $this->getDetail($campise['UnitID']); } if(!$res||isset($this->result[$campise['UnitID']])){ continue; } $dataDetail = $this->dataDetail; //校名 $this->result[$campise['UnitID']]['campuse'] = $campise['Name']; foreach ($dataDetail as $k => $data) { if ($k == 0) continue; $this->result[$campise['UnitID']]['data'][] = empty($data) ? [] : array_column($data['Cells'], 'Html'); } } } /** * 检查学校名 * @return array */ private function checkName() { $school = json_decode($this->httpRequest($this->schoolApi . urlencode($this->schoolName)), true); if (!isset($school[0]['Code']) || !$school[0]['Code']) { $this->error = '检查学校名 error'; return false; } return true; } /** * 获取校区 * @return array */ private function getCampises() { $schoolSerachData = "{\"name\":\"" . $this->schoolName . "\",\"city\":\"\",\"state\":[],\"country\":[],\"countryNames\":[],\"institutionType\":[],\"institutionProgram\":[],\"campusLocation\":\"-1\",\"onlyResidentialCampuses\":false,\"enrollmentRange\":[],\"sort\":\"name\",\"sortDirection\":\"asc\",\"all\":false,\"pageNumber\":0,\"fromFavorites\":false}"; //获取校区列表 $campisesData = json_decode($this->httpRequest($this->schoolSerachApi, $schoolSerachData), true); if (!isset($campisesData['Results'][0]['Campuses']) || empty($campisesData['Results'][0]['Campuses'])) { $this->error = '获取校区 error'; return false; } $this->campisesData = $campisesData['Results']; return true; } /** * @name 获取全部详情 * @param $unitId * @return array */ private function getDetail($unitId) { $dataDetail = json_decode($this->httpRequest($this->totalApi . $unitId), true); if (!isset($dataDetail['Groups'][0]['Screens'][0]['Rows']) || empty($dataDetail['Groups'][0]['Screens'][0]['Rows'])) { $this->error = '获取详情 error'; return false; } $this->dataDetail = $dataDetail['Groups'][0]['Screens'][0]['Rows']; return true; } /** * @name 获取分校区详情 * @param $unitId * @return array */ private function getDetailCampuse($unitId) { $dataDetail = json_decode($this->httpRequest($this->dataApi . $unitId), true); if (!isset($dataDetail['Groups'][0]['Screens'][0]['Rows']) || empty($dataDetail['Groups'][0]['Screens'][0]['Rows'])) { $this->error = '获取详情 error'; return false; } $this->dataDetail = $dataDetail['Groups'][0]['Screens'][0]['Rows']; return true; } /** * @name 请求方法 * @param $url * @param $data * @return false|mixed|string */ private function httpRequest($url, $data = '') { $curl = curl_init(); $method = $data ? 'POST' : 'GET'; curl_setopt_array($curl, array( CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => "", CURLOPT_MAXREDIRS => 10, CURLOPT_TIMEOUT => 30, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_POSTFIELDS => $data, CURLOPT_HTTPHEADER => array( "Content-Type: application/json;charset=UTF-8", "Postman-Token: a30f51b0-cc09-4bfb-801e-830a931994f4", "cache-control: no-cache" ), )); $response = curl_exec($curl); $err = curl_error($curl); curl_close($curl); if ($err) { echo "cURL Error #:" . $err; return json_encode([]); } else { return strip_tags($response); } } } $a = new SchoolSpider(); $r = $a->spiderData('Harvard University'); echo json_encode($r);
Clean Code PHP目录介绍变量使用见字知意的变量名同一个实体要用相同的变量名使用便于搜索的名称 (part 1)使用便于搜索的名称 (part 2)使用自解释型变量避免深层嵌套，尽早返回 (part 1)避免深层嵌套，尽早返回 (part 2)少用无意义的变量名不要添加不必要上下文合理使用参数默认值，没必要在方法里再做默认值检测表达式使用恒等式函数函数参数（最好少于2个）函数应该只做一件事函数名应该是有意义的动词（或表明具体做了什么事）函数里应当只有一层抽象abstraction不要用flag作为函数的参数避免副作用不要写全局函数不要使用单例模式封装条件语句避免用反义条件判断避免条件判断避免类型检查 (part 1)避免类型检查 (part 2)移除僵尸代码对象和数据结构 Objects and Data Structures使用 getters 和 setters Use object encapsulation对象属性多使用private/protected限定类组合优于继承避免连贯接口推荐使用 final 类类的SOLID原则 SOLIDS: 职责单一原则 Single Responsibility Principle (SRP)O: 开闭原则 Open/Closed Principle (OCP)L: 里氏替换原则 Liskov Substitution Principle (LSP)I: 接口隔离原则 Interface Segregation Principle (ISP)D: 依赖反转原则 Dependency Inversion Principle (DIP)别写重复代码 (DRY)翻译介绍本文参考自 Robert C. Martin的Clean Code 书中的软件工程师的原则,适用于PHP。 这不是风格指南。 这是一个关于开发可读、可复用并且可重构的PHP软件指南。并不是这里所有的原则都得遵循，甚至很少的能被普遍接受。 这些虽然只是指导，但是都是Clean Code作者多年总结出来的。本文受到 clean-code-javascript 的启发虽然很多开发者还在使用PHP5，但是本文中的大部分示例的运行环境需要PHP 7.1+。翻译说明翻译完成度100%，最后更新时间2017-12-25。本文由 php-cpm 基于 yangweijie版本 的clean-code-php翻译并同步大量原文内容。原文更新频率较高，我的翻译方法是直接用文本比较工具逐行对比。优先保证文字内容是最新的，再逐步提升翻译质量。阅读过程中如果遇到各种链接失效、内容老旧、术语使用错误和其他翻译错误等问题，欢迎大家积极提交PR。变量使用见字知意的变量名坏:$ymdstr = $moment->format('y-m-d');好:$currentDate = $moment->format('y-m-d');⬆ 返回顶部同一个实体要用相同的变量名坏:getUserInfo(); getUserData(); getUserRecord(); getUserProfile();好:getUser();⬆ 返回顶部使用便于搜索的名称 (part 1)写代码是用来读的。所以写出可读性高、便于搜索的代码至关重要。命名变量时如果没有有意义、不好理解，那就是在伤害读者。请让你的代码便于搜索。坏:// What the heck is 448 for? $result = $serializer->serialize($data, 448);好:$json = $serializer->serialize($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);使用便于搜索的名称 (part 2)坏:// What the heck is 4 for? if ($user->access & 4) { // ... }好:class User { const ACCESS_READ = 1; const ACCESS_CREATE = 2; const ACCESS_UPDATE = 4; const ACCESS_DELETE = 8; } if ($user->access & User::ACCESS_UPDATE) { // do edit ... }⬆ 返回顶部使用自解释型变量坏:$address = 'One Infinite Loop, Cupertino 95014'; $cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/'; preg_match($cityZipCodeRegex, $address, $matches); saveCityZipCode($matches[1], $matches[2]);不错:好一些，但强依赖于正则表达式的熟悉程度$address = 'One Infinite Loop, Cupertino 95014'; $cityZipCodeRegex = '/^[^,]+,\s*(.+?)\s*(\d{5})$/'; preg_match($cityZipCodeRegex, $address, $matches); [, $city, $zipCode] = $matches; saveCityZipCode($city, $zipCode);好:使用带名字的子规则，不用懂正则也能看的懂$address = 'One Infinite Loop, Cupertino 95014'; $cityZipCodeRegex = '/^[^,]+,\s*(?<city>.+?)\s*(?<zipCode>\d{5})$/'; preg_match($cityZipCodeRegex, $address, $matches); saveCityZipCode($matches['city'], $matches['zipCode']);⬆ 返回顶部避免深层嵌套，尽早返回 (part 1)太多的if else语句通常会导致你的代码难以阅读，直白优于隐晦糟糕:function isShopOpen($day): bool { if ($day) { if (is_string($day)) { $day = strtolower($day); if ($day === 'friday') { return true; } elseif ($day === 'saturday') { return true; } elseif ($day === 'sunday') { return true; } else { return false; } } else { return false; } } else { return false; } }好:function isShopOpen(string $day): bool { if (empty($day)) { return false; } $openingDays = [ 'friday', 'saturday', 'sunday' ]; return in_array(strtolower($day), $openingDays, true); }⬆ 返回顶部避免深层嵌套，尽早返回 (part 2)糟糕的:function fibonacci(int $n) { if ($n < 50) { if ($n !== 0) { if ($n !== 1) { return fibonacci($n - 1) + fibonacci($n - 2); } else { return 1; } } else { return 0; } } else { return 'Not supported'; } }好:function fibonacci(int $n): int { if ($n === 0 || $n === 1) { return $n; } if ($n > 50) { throw new \Exception('Not supported'); } return fibonacci($n - 1) + fibonacci($n - 2); }⬆ 返回顶部少用无意义的变量名别让读你的代码的人猜你写的变量是什么意思。写清楚好过模糊不清。坏:$l = ['Austin', 'New York', 'San Francisco']; for ($i = 0; $i < count($l); $i++) { $li = $l[$i]; doStuff(); doSomeOtherStuff(); // ... // ... // ... // 等等, `$li` 又代表什么? dispatch($li); }好:$locations = ['Austin', 'New York', 'San Francisco']; foreach ($locations as $location) { doStuff(); doSomeOtherStuff(); // ... // ... // ... dispatch($location); }⬆ 返回顶部不要添加不必要上下文如果从你的类名、对象名已经可以得知一些信息，就别再在变量名里重复。坏:class Car { public $carMake; public $carModel; public $carColor; //... }好:class Car { public $make; public $model; public $color; //... }⬆ 返回顶部合理使用参数默认值，没必要在方法里再做默认值检测不好:不好，$breweryName 可能为 NULL.function createMicrobrewery($breweryName = 'Hipster Brew Co.'): void { // ... }还行:比上一个好理解一些，但最好能控制变量的值function createMicrobrewery($name = null): void { $breweryName = $name ?: 'Hipster Brew Co.'; // ... }好:如果你的程序只支持 PHP 7+, 那你可以用 type hinting 保证变量 $breweryName 不是 NULL.function createMicrobrewery(string $breweryName = 'Hipster Brew Co.'): void { // ... }⬆ 返回顶部表达式使用恒等式不好:简易对比会将字符串转为整形$a = '42'; $b = 42; if( $a != $b ) { //这里始终执行不到 }对比 $a != $b 返回了 FALSE 但应该返回 TRUE !字符串 '42' 跟整数 42 不相等好:使用恒等判断检查类型和数据$a = '42'; $b = 42; if ($a !== $b) { // The expression is verified }The comparison $a !== $b returns TRUE.⬆ 返回顶部函数函数参数（最好少于2个）限制函数参数个数极其重要，这样测试你的函数容易点。有超过3个可选参数参数导致一个爆炸式组合增长，你会有成吨独立参数情形要测试。无参数是理想情况。1个或2个都可以，最好避免3个。再多就需要加固了。通常如果你的函数有超过两个参数，说明他要处理的事太多了。 如果必须要传入很多数据，建议封装一个高级别对象作为参数。坏:function createMenu(string $title, string $body, string $buttonText, bool $cancellable): void { // ... }好:class MenuConfig { public $title; public $body; public $buttonText; public $cancellable = false; } $config = new MenuConfig(); $config->title = 'Foo'; $config->body = 'Bar'; $config->buttonText = 'Baz'; $config->cancellable = true; function createMenu(MenuConfig $config): void { // ... }⬆ 返回顶部函数应该只做一件事这是迄今为止软件工程里最重要的一个规则。当一个函数做超过一件事的时候，他们就难于实现、测试和理解。当你把一个函数拆分到只剩一个功能时，他们就容易被重构，然后你的代码读起来就更清晰。如果你光遵循这条规则，你就领先于大多数开发者了。坏:function emailClients(array $clients): void { foreach ($clients as $client) { $clientRecord = $db->find($client); if ($clientRecord->isActive()) { email($client); } } }好:function emailClients(array $clients): void { $activeClients = activeClients($clients); array_walk($activeClients, 'email'); } function activeClients(array $clients): array { return array_filter($clients, 'isClientActive'); } function isClientActive(int $client): bool { $clientRecord = $db->find($client); return $clientRecord->isActive(); }⬆ 返回顶部函数名应该是有意义的动词（或表明具体做了什么事）坏:class Email { //... public function handle(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // 啥？handle处理一个消息干嘛了？是往一个文件里写码？ $message->handle();好:class Email { //... public function send(): void { mail($this->to, $this->subject, $this->body); } } $message = new Email(...); // 简单明了 $message->send();⬆ 返回顶部函数里应当只有一层抽象abstraction当你抽象层次过多时时，函数处理的事情太多了。需要拆分功能来提高可重用性和易用性，以便简化测试。（译者注：这里从示例代码看应该是指嵌套过多）坏:function parseBetterJSAlternative(string $code): void { $regexes = [ // ... ]; $statements = explode(' ', $code); $tokens = []; foreach ($regexes as $regex) { foreach ($statements as $statement) { // ... } } $ast = []; foreach ($tokens as $token) { // lex... } foreach ($ast as $node) { // parse... } }坏:我们把一些方法从循环中提取出来，但是parseBetterJSAlternative()方法还是很复杂，而且不利于测试。function tokenize(string $code): array { $regexes = [ // ... ]; $statements = explode(' ', $code); $tokens = []; foreach ($regexes as $regex) { foreach ($statements as $statement) { $tokens[] = /* ... */; } } return $tokens; } function lexer(array $tokens): array { $ast = []; foreach ($tokens as $token) { $ast[] = /* ... */; } return $ast; } function parseBetterJSAlternative(string $code): void { $tokens = tokenize($code); $ast = lexer($tokens); foreach ($ast as $node) { // 解析逻辑... } }好:最好的解决方案是把 parseBetterJSAlternative()方法的依赖移除。class Tokenizer { public function tokenize(string $code): array { $regexes = [ // ... ]; $statements = explode(' ', $code); $tokens = []; foreach ($regexes as $regex) { foreach ($statements as $statement) { $tokens[] = /* ... */; } } return $tokens; } } class Lexer { public function lexify(array $tokens): array { $ast = []; foreach ($tokens as $token) { $ast[] = /* ... */; } return $ast; } } class BetterJSAlternative { private $tokenizer; private $lexer; public function __construct(Tokenizer $tokenizer, Lexer $lexer) { $this->tokenizer = $tokenizer; $this->lexer = $lexer; } public function parse(string $code): void { $tokens = $this->tokenizer->tokenize($code); $ast = $this->lexer->lexify($tokens); foreach ($ast as $node) { // 解析逻辑... } } }这样我们可以对依赖做mock，并测试BetterJSAlternative::parse()运行是否符合预期。⬆ 返回顶部不要用flag作为函数的参数flag就是在告诉大家，这个方法里处理很多事。前面刚说过，一个函数应当只做一件事。 把不同flag的代码拆分到多个函数里。坏:function createFile(string $name, bool $temp = false): void { if ($temp) { touch('./temp/'.$name); } else { touch($name); } }好:function createFile(string $name): void { touch($name); } function createTempFile(string $name): void { touch('./temp/'.$name); }⬆ 返回顶部避免副作用一个函数做了比获取一个值然后返回另外一个值或值们会产生副作用如果。副作用可能是写入一个文件，修改某些全局变量或者偶然的把你全部的钱给了陌生人。现在，你的确需要在一个程序或者场合里要有副作用，像之前的例子，你也许需要写一个文件。你想要做的是把你做这些的地方集中起来。不要用几个函数和类来写入一个特定的文件。用一个服务来做它，一个只有一个。重点是避免常见陷阱比如对象间共享无结构的数据，使用可以写入任何的可变数据类型，不集中处理副作用发生的地方。如果你做了这些你就会比大多数程序员快乐。坏:// Global variable referenced by following function. // If we had another function that used this name, now it'd be an array and it could break it. $name = 'Ryan McDermott'; function splitIntoFirstAndLastName(): void { global $name; $name = explode(' ', $name); } splitIntoFirstAndLastName(); var_dump($name); // ['Ryan', 'McDermott'];好:function splitIntoFirstAndLastName(string $name): array { return explode(' ', $name); } $name = 'Ryan McDermott'; $newName = splitIntoFirstAndLastName($name); var_dump($name); // 'Ryan McDermott'; var_dump($newName); // ['Ryan', 'McDermott'];⬆ 返回顶部不要写全局函数在大多数语言中污染全局变量是一个坏的实践，因为你可能和其他类库冲突并且调用你api的人直到他们捕获异常才知道踩坑了。让我们思考一种场景：如果你想配置一个数组，你可能会写一个全局函数config()，但是他可能和试着做同样事的其他类库冲突。坏:function config(): array { return [ 'foo' => 'bar', ] }好:class Configuration { private $configuration = []; public function __construct(array $configuration) { $this->configuration = $configuration; } public function get(string $key): ?string { return isset($this->configuration[$key]) ? $this->configuration[$key] : null; } }加载配置并创建 Configuration 类的实例$configuration = new Configuration([ 'foo' => 'bar', ]);现在你必须在程序中用 Configuration 的实例了⬆ 返回顶部不要使用单例模式单例是一种 反模式. 以下是解释：Paraphrased from Brian Button:总是被用成全局实例。They are generally used as a global instance, why is that so bad? Because you hide the dependencies of your application in your code, instead of exposing them through the interfaces. Making something global to avoid passing it around is a code smell.违反了[单一响应原则]()They violate the single responsibility principle: by virtue of the fact that they control their own creation and lifecycle.导致代码强耦合They inherently cause code to be tightly coupled. This makes faking them out under test rather difficult in many cases.在整个程序的生命周期中始终携带状态。They carry state around for the lifetime of the application. Another hit to testing since you can end up with a situation where tests need to be ordered which is a big no for unit tests. Why? Because each unit test should be independent from the other.这里有一篇非常好的讨论单例模式的根本问题((http://misko.hevery.com/2008/08/25/root-cause-of-singletons/)的文章，是[Misko Hevery 写的。坏:class DBConnection { private static $instance; private function __construct(string $dsn) { // ... } public static function getInstance(): DBConnection { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } // ... } $singleton = DBConnection::getInstance();好:class DBConnection { public function __construct(string $dsn) { // ... } // ... }创建 DBConnection 类的实例并通过 DSN 配置.$connection = new DBConnection($dsn);现在你必须在程序中 使用 DBConnection 的实例了⬆ 返回顶部封装条件语句坏:if ($article->state === 'published') { // ... }好:if ($article->isPublished()) { // ... }⬆ 返回顶部避免用反义条件判断坏:function isDOMNodeNotPresent(\DOMNode $node): bool { // ... } if (!isDOMNodeNotPresent($node)) { // ... }好:function isDOMNodePresent(\DOMNode $node): bool { // ... } if (isDOMNodePresent($node)) { // ... }⬆ 返回顶部避免条件判断这看起来像一个不可能任务。当人们第一次听到这句话是都会这么说。"没有if语句我还能做啥？" 答案是你可以使用多态来实现多种场景的相同任务。第二个问题很常见， “这么做可以，但为什么我要这么做？” 答案是前面我们学过的一个Clean Code原则：一个函数应当只做一件事。 当你有很多含有if语句的类和函数时,你的函数做了不止一件事。 记住，只做一件事。坏:class Airplane { // ... public function getCruisingAltitude(): int { switch ($this->type) { case '777': return $this->getMaxAltitude() - $this->getPassengerCount(); case 'Air Force One': return $this->getMaxAltitude(); case 'Cessna': return $this->getMaxAltitude() - $this->getFuelExpenditure(); } } }好:interface Airplane { // ... public function getCruisingAltitude(): int; } class Boeing777 implements Airplane { // ... public function getCruisingAltitude(): int { return $this->getMaxAltitude() - $this->getPassengerCount(); } } class AirForceOne implements Airplane { // ... public function getCruisingAltitude(): int { return $this->getMaxAltitude(); } } class Cessna implements Airplane { // ... public function getCruisingAltitude(): int { return $this->getMaxAltitude() - $this->getFuelExpenditure(); } }⬆ 返回顶部避免类型检查 (part 1)PHP是弱类型的,这意味着你的函数可以接收任何类型的参数。有时候你为这自由所痛苦并且在你的函数渐渐尝试类型检查。有很多方法去避免这么做。第一种是统一API。坏:function travelToTexas($vehicle): void { if ($vehicle instanceof Bicycle) { $vehicle->pedalTo(new Location('texas')); } elseif ($vehicle instanceof Car) { $vehicle->driveTo(new Location('texas')); } }好:function travelToTexas(Traveler $vehicle): void { $vehicle->travelTo(new Location('texas')); }⬆ 返回顶部避免类型检查 (part 2)如果你正使用基本原始值比如字符串、整形和数组，要求版本是PHP 7+，不用多态，需要类型检测，那你应当考虑类型声明或者严格模式。提供了基于标准PHP语法的静态类型。 手动检查类型的问题是做好了需要好多的废话，好像为了安全就可以不顾损失可读性。保持你的PHP 整洁，写好测试，做好代码回顾。做不到就用PHP严格类型声明和严格模式来确保安全。坏:function combine($val1, $val2): int { if (!is_numeric($val1) || !is_numeric($val2)) { throw new \Exception('Must be of type Number'); } return $val1 + $val2; }好:function combine(int $val1, int $val2): int { return $val1 + $val2; }⬆ 返回顶部移除僵尸代码僵尸代码和重复代码一样坏。没有理由保留在你的代码库中。如果从来没被调用过，就删掉！因为还在代码版本库里，因此很安全。坏:function oldRequestModule(string $url): void { // ... } function newRequestModule(string $url): void { // ... } $request = newRequestModule($requestUrl); inventoryTracker('apples', $request, 'www.inventory-awesome.io');好:function requestModule(string $url): void { // ... } $request = requestModule($requestUrl); inventoryTracker('apples', $request, 'www.inventory-awesome.io');⬆ 返回顶部对象和数据结构使用 getters 和 setters在PHP中你可以对方法使用public, protected, private 来控制对象属性的变更。当你想对对象属性做获取之外的操作时，你不需要在代码中去寻找并修改每一个该属性访问方法当有set对应的属性方法时，易于增加参数的验证封装内部的表示使用set和get时，易于增加日志和错误控制继承当前类时，可以复写默认的方法功能当对象属性是从远端服务器获取时，get，set易于使用延迟加载此外，这样的方式也符合OOP开发中的开闭原则坏:class BankAccount { public $balance = 1000; } $bankAccount = new BankAccount(); // Buy shoes... $bankAccount->balance -= 100;好:class BankAccount { private $balance; public function __construct(int $balance = 1000) { $this->balance = $balance; } public function withdraw(int $amount): void { if ($amount > $this->balance) { throw new \Exception('Amount greater than available balance.'); } $this->balance -= $amount; } public function deposit(int $amount): void { $this->balance += $amount; } public function getBalance(): int { return $this->balance; } } $bankAccount = new BankAccount(); // Buy shoes... $bankAccount->withdraw($shoesPrice); // Get balance $balance = $bankAccount->getBalance();⬆ 返回顶部对象属性多使用private/protected限定对public方法和属性进行修改非常危险，因为外部代码容易依赖他，而你没办法控制。对之修改影响所有这个类的使用者。 public methods and properties are most dangerous for changes, because some outside code may easily rely on them and you can't control what code relies on them. Modifications in class are dangerous for all users of class.对protected的修改跟对public修改差不多危险，因为他们对子类可用，他俩的唯一区别就是可调用的位置不一样，对之修改影响所有集成这个类的地方。 protected modifier are as dangerous as public, because they are available in scope of any child class. This effectively means that difference between public and protected is only in access mechanism, but encapsulation guarantee remains the same. Modifications in class are dangerous for all descendant classes.对private的修改保证了这部分代码只会影响当前类private modifier guarantees that code is dangerous to modify only in boundaries of single class (you are safe for modifications and you won't have Jenga effect).所以，当你需要控制类里的代码可以被访问时才用public/protected，其他时候都用private。可以读一读这篇 博客文章 ，Fabien Potencier写的.坏:class Employee { public $name; public function __construct(string $name) { $this->name = $name; } } $employee = new Employee('John Doe'); echo 'Employee name: '.$employee->name; // Employee name: John Doe好:class Employee { private $name; public function __construct(string $name) { $this->name = $name; } public function getName(): string { return $this->name; } } $employee = new Employee('John Doe'); echo 'Employee name: '.$employee->getName(); // Employee name: John Doe⬆ 返回顶部类组合优于继承正如 the Gang of Four 所著的设计模式之前所说，我们应该尽量优先选择组合而不是继承的方式。使用继承和组合都有很多好处。这个准则的主要意义在于当你本能的使用继承时，试着思考一下组合是否能更好对你的需求建模。在一些情况下，是这样的。接下来你或许会想，“那我应该在什么时候使用继承？” 答案依赖于你的问题，当然下面有一些何时继承比组合更好的说明：你的继承表达了“是一个”而不是“有一个”的关系（人类-》动物，用户-》用户详情）你可以复用基类的代码（人类可以像动物一样移动）你想通过修改基类对所有派生类做全局的修改（当动物移动时，修改她们的能量消耗）糟糕的:class Employee { private $name; private $email; public function __construct(string $name, string $email) { $this->name = $name; $this->email = $email; } // ... } // 不好，因为Employees "有" taxdata // 而EmployeeTaxData不是Employee类型的 class EmployeeTaxData extends Employee { private $ssn; private $salary; public function __construct(string $name, string $email, string $ssn, string $salary) { parent::__construct($name, $email); $this->ssn = $ssn; $this->salary = $salary; } // ... }好:class EmployeeTaxData { private $ssn; private $salary; public function __construct(string $ssn, string $salary) { $this->ssn = $ssn; $this->salary = $salary; } // ... } class Employee { private $name; private $email; private $taxData; public function __construct(string $name, string $email) { $this->name = $name; $this->email = $email; } public function setTaxData(string $ssn, string $salary) { $this->taxData = new EmployeeTaxData($ssn, $salary); } // ... }⬆ 返回顶部避免连贯接口连贯接口Fluent interface是一种旨在提高面向对象编程时代码可读性的API设计模式，他基于方法链Method chaining有上下文的地方可以降低代码复杂度，例如PHPUnit Mock Builder和Doctrine Query Builder，更多的情况会带来较大代价：While there can be some contexts, frequently builder objects, where thispattern reduces the verbosity of the code (for example the PHPUnit Mock Builderor the Doctrine Query Builder),more often it comes at some costs:破坏了 对象封装破坏了 装饰器模式在测试组件中不好做mock导致提交的diff不好阅读了解更多请阅读 连贯接口为什么不好，作者 Marco Pivetta.坏:class Car { private $make = 'Honda'; private $model = 'Accord'; private $color = 'white'; public function setMake(string $make): self { $this->make = $make; // NOTE: Returning this for chaining return $this; } public function setModel(string $model): self { $this->model = $model; // NOTE: Returning this for chaining return $this; } public function setColor(string $color): self { $this->color = $color; // NOTE: Returning this for chaining return $this; } public function dump(): void { var_dump($this->make, $this->model, $this->color); } } $car = (new Car()) ->setColor('pink') ->setMake('Ford') ->setModel('F-150') ->dump();好:class Car { private $make = 'Honda'; private $model = 'Accord'; private $color = 'white'; public function setMake(string $make): void { $this->make = $make; } public function setModel(string $model): void { $this->model = $model; } public function setColor(string $color): void { $this->color = $color; } public function dump(): void { var_dump($this->make, $this->model, $this->color); } } $car = new Car(); $car->setColor('pink'); $car->setMake('Ford'); $car->setModel('F-150'); $car->dump();⬆ 返回顶部推荐使用 final 类能用时尽量使用 final 关键字:阻止不受控的继承链鼓励 组合.鼓励 单一职责模式.鼓励开发者用你的公开方法而非通过继承类获取受保护方法的访问权限.使得在不破坏使用你的类的应用的情况下修改代码成为可能.The only condition is that your class should implement an interface and no other public methods are defined.For more informations you can read the blog post on this topic written by Marco Pivetta (Ocramius).Bad:final class Car { private $color; public function __construct($color) { $this->color = $color; } /** * @return string The color of the vehicle */ public function getColor() { return $this->color; } }Good:interface Vehicle { /** * @return string The color of the vehicle */ public function getColor(); } final class Car implements Vehicle { private $color; public function __construct($color) { $this->color = $color; } /** * {@inheritdoc} */ public function getColor() { return $this->color; } }⬆ 返回顶部SOLIDSOLID 是Michael Feathers推荐的便于记忆的首字母简写，它代表了Robert Martin命名的最重要的五个面对对象编码设计原则S: 职责单一原则 (SRP)O: 开闭原则 (OCP)L: 里氏替换原则 (LSP)I: 接口隔离原则 (ISP)D: 依赖反转原则 (DIP)职责单一原则 Single Responsibility Principle (SRP)正如在Clean Code所述，"修改一个类应该只为一个理由"。人们总是易于用一堆方法塞满一个类，如同我们只能在飞机上只能携带一个行李箱（把所有的东西都塞到箱子里）。这样做的问题是：从概念上这样的类不是高内聚的，并且留下了很多理由去修改它。将你需要修改类的次数降低到最小很重要。这是因为，当有很多方法在类中时，修改其中一处，你很难知晓在代码库中哪些依赖的模块会被影响到。坏:class UserSettings { private $user; public function __construct(User $user) { $this->user = $user; } public function changeSettings(array $settings): void { if ($this->verifyCredentials()) { // ... } } private function verifyCredentials(): bool { // ... } }好:class UserAuth { private $user; public function __construct(User $user) { $this->user = $user; } public function verifyCredentials(): bool { // ... } } class UserSettings { private $user; private $auth; public function __construct(User $user) { $this->user = $user; $this->auth = new UserAuth($user); } public function changeSettings(array $settings): void { if ($this->auth->verifyCredentials()) { // ... } } }⬆ 返回顶部开闭原则 Open/Closed Principle (OCP)正如Bertrand Meyer所述，"软件的工件（classes, modules, functions,等）应该对扩展开放，对修改关闭。" 然而这句话意味着什么呢？这个原则大体上表示你应该允许在不改变已有代码的情况下增加新的功能坏:abstract class Adapter { protected $name; public function getName(): string { return $this->name; } } class AjaxAdapter extends Adapter { public function __construct() { parent::__construct(); $this->name = 'ajaxAdapter'; } } class NodeAdapter extends Adapter { public function __construct() { parent::__construct(); $this->name = 'nodeAdapter'; } } class HttpRequester { private $adapter; public function __construct(Adapter $adapter) { $this->adapter = $adapter; } public function fetch(string $url): Promise { $adapterName = $this->adapter->getName(); if ($adapterName === 'ajaxAdapter') { return $this->makeAjaxCall($url); } elseif ($adapterName === 'httpNodeAdapter') { return $this->makeHttpCall($url); } } private function makeAjaxCall(string $url): Promise { // request and return promise } private function makeHttpCall(string $url): Promise { // request and return promise } }好:interface Adapter { public function request(string $url): Promise; } class AjaxAdapter implements Adapter { public function request(string $url): Promise { // request and return promise } } class NodeAdapter implements Adapter { public function request(string $url): Promise { // request and return promise } } class HttpRequester { private $adapter; public function __construct(Adapter $adapter) { $this->adapter = $adapter; } public function fetch(string $url): Promise { return $this->adapter->request($url); } }⬆ 返回顶部里氏替换原则 Liskov Substitution Principle (LSP)这是一个简单的原则，却用了一个不好理解的术语。它的正式定义是"如果S是T的子类型，那么在不改变程序原有既定属性（检查、执行任务等）的前提下，任何T类型的对象都可以使用S类型的对象替代（例如，使用S的对象可以替代T的对象）" 这个定义更难理解:-)。对这个概念最好的解释是：如果你有一个父类和一个子类，在不改变原有结果正确性的前提下父类和子类可以互换。这个听起来依旧让人有些迷惑，所以让我们来看一个经典的正方形-长方形的例子。从数学上讲，正方形是一种长方形，但是当你的模型通过继承使用了"is-a"的关系时，就不对了。坏:class Rectangle { protected $width = 0; protected $height = 0; public function setWidth(int $width): void { $this->width = $width; } public function setHeight(int $height): void { $this->height = $height; } public function getArea(): int { return $this->width * $this->height; } } class Square extends Rectangle { public function setWidth(int $width): void { $this->width = $this->height = $width; } public function setHeight(int $height): void { $this->width = $this->height = $height; } } function printArea(Rectangle $rectangle): void { $rectangle->setWidth(4); $rectangle->setHeight(5); // BAD: Will return 25 for Square. Should be 20. echo sprintf('%s has area %d.', get_class($rectangle), $rectangle->getArea()).PHP_EOL; } $rectangles = [new Rectangle(), new Square()]; foreach ($rectangles as $rectangle) { printArea($rectangle); }好:最好是将这两种四边形分别对待，用一个适合两种类型的更通用子类型来代替。尽管正方形和长方形看起来很相似，但他们是不同的。正方形更接近菱形，而长方形更接近平行四边形。但他们不是子类型。尽管相似，正方形、长方形、菱形、平行四边形都是有自己属性的不同形状。interface Shape { public function getArea(): int; } class Rectangle implements Shape { private $width = 0; private $height = 0; public function __construct(int $width, int $height) { $this->width = $width; $this->height = $height; } public function getArea(): int { return $this->width * $this->height; } } class Square implements Shape { private $length = 0; public function __construct(int $length) { $this->length = $length; } public function getArea(): int { return $this->length ** 2; } } function printArea(Shape $shape): void { echo sprintf('%s has area %d.', get_class($shape), $shape->getArea()).PHP_EOL; } $shapes = [new Rectangle(4, 5), new Square(5)]; foreach ($shapes as $shape) { printArea($shape); }⬆ 返回顶部接口隔离原则 Interface Segregation Principle (ISP)接口隔离原则表示："调用方不应该被强制依赖于他不需要的接口"有一个清晰的例子来说明示范这条原则。当一个类需要一个大量的设置项，为了方便不会要求调用方去设置大量的选项，因为在通常他们不需要所有的设置项。使设置项可选有助于我们避免产生"胖接口"坏:interface Employee { public function work(): void; public function eat(): void; } class Human implements Employee { public function work(): void { // ....working } public function eat(): void { // ...... eating in lunch break } } class Robot implements Employee { public function work(): void { //.... working much more } public function eat(): void { //.... robot can't eat, but it must implement this method } }好:不是每一个工人都是雇员，但是每一个雇员都是一个工人interface Workable { public function work(): void; } interface Feedable { public function eat(): void; } interface Employee extends Feedable, Workable { } class Human implements Employee { public function work(): void { // ....working } public function eat(): void { //.... eating in lunch break } } // robot can only work class Robot implements Workable { public function work(): void { // ....working } }⬆ 返回顶部依赖反转原则 Dependency Inversion Principle (DIP)这条原则说明两个基本的要点：高阶的模块不应该依赖低阶的模块，它们都应该依赖于抽象抽象不应该依赖于实现，实现应该依赖于抽象这条起初看起来有点晦涩难懂，但是如果你使用过php框架（例如 Symfony），你应该见过依赖注入（DI）对这个概念的实现。虽然它们不是完全相通的概念，依赖倒置原则使高阶模块与低阶模块的实现细节和创建分离。可以使用依赖注入（DI）这种方式来实现它。更多的好处是它使模块之间解耦。耦合会导致你难于重构，它是一种非常糟糕的的开发模式坏:class Employee { public function work(): void { // ....working } } class Robot extends Employee { public function work(): void { //.... working much more } } class Manager { private $employee; public function __construct(Employee $employee) { $this->employee = $employee; } public function manage(): void { $this->employee->work(); } }好:interface Employee { public function work(): void; } class Human implements Employee { public function work(): void { // ....working } } class Robot implements Employee { public function work(): void { //.... working much more } } class Manager { private $employee; public function __construct(Employee $employee) { $this->employee = $employee; } public function manage(): void { $this->employee->work(); } }⬆ 返回顶部别写重复代码 (DRY)试着去遵循DRY 原则.尽你最大的努力去避免复制代码，它是一种非常糟糕的行为，复制代码通常意味着当你需要变更一些逻辑时，你需要修改不止一处。试想一下，如果你在经营一家餐厅并且你在记录你仓库的进销记录：所有的土豆，洋葱，大蒜，辣椒等。如果你有多个列表来管理进销记录，当你用其中一些土豆做菜时你需要更新所有的列表。如果你只有一个列表的话只有一个地方需要更新。通常情况下你复制代码是应该有两个或者多个略微不同的逻辑，它们大多数都是一样的，但是由于它们的区别致使你必须有两个或者多个隔离的但大部分相同的方法，移除重复的代码意味着用一个function/module/class创建一个能处理差异的抽象。正确的抽象是非常关键的，这正是为什么你必须学习遵守在Classes章节展开的SOLID原则，不合理的抽象比复制代码更糟糕，所有务必谨慎！说到这么多，如果你能设计一个合理的抽象，实现它！不要重复，否则你会发现任何时候当你想修改一个逻辑时你必须修改多个地方。坏:function showDeveloperList(array $developers): void { foreach ($developers as $developer) { $expectedSalary = $developer->calculateExpectedSalary(); $experience = $developer->getExperience(); $githubLink = $developer->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); } } function showManagerList(array $managers): void { foreach ($managers as $manager) { $expectedSalary = $manager->calculateExpectedSalary(); $experience = $manager->getExperience(); $githubLink = $manager->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); } }好:function showList(array $employees): void { foreach ($employees as $employee) { $expectedSalary = $employee->calculateExpectedSalary(); $experience = $employee->getExperience(); $githubLink = $employee->getGithubLink(); $data = [ $expectedSalary, $experience, $githubLink ]; render($data); } }极好:最好让代码紧凑一点function showList(array $employees): void { foreach ($employees as $employee) { render([ $employee->calculateExpectedSalary(), $employee->getExperience(), $employee->getGithubLink() ]); } }⬆ 返回顶部翻译其他语言的翻译::cn: Chinese:php-cpm/clean-code-php:ru: Russian:peter-gribanov/clean-code-php:es: Spanish:fikoborquez/clean-code-php:brazil: Portuguese:fabioars/clean-code-phpjeanjar/clean-code-php:thailand: Thai:panuwizzle/clean-code-php:fr: French:errorname/clean-code-php:vietnam: Vietnameseviethuongdev/clean-code-php:kr: Korean:yujineeee/clean-code-php⬆ 返回顶部
1.命令行界面命令行界面（英语：Command-lineInterface，缩写：CLI）是在图形用户界面得到普及之前使用最为广泛的用户界面，它通常不支持鼠标，用户通过键盘输入指令，计算机接收到指令后，予以执行。2.终端在大型机 (Mainframe) 和小型机 (Minicomputer)的时代里，计算机曾经非常昂贵且巨大，不像现在这样人手一台。这些笨重的计算机通常被安置在单独的房间内，而操作计算机的人们坐在另外的房间里，通过某些设备与计算机进行交互。这种设备就叫做 终端 (Terminal)，也叫终端机。2.1控制台(console)历史上，终端是链接到计算机的一种带输入输出功能的外设。但是有一种终端于总不同，它和计算机是一体的，是计算机的一部分。这个特殊的终端就是 控制台（console）。2.2终端模拟器 (Terminal Emulator)随着计算机的进化，我们已经见不到专门的终端硬件了，取而代之的则是键盘与显示器。但是没有了终端，我们要怎么与那些传统的、不兼容图形接口的命令行程序（比如说 GNU 工具集里的大部分命令）交互呢？这些程序并不能直接读取我们的键盘输入，也没办法把计算结果显示在我们的显示器上……（图形界面的原理我这里就不多说了，它们编程的时候图形接口还在娘胎里呢！）这时候我们就需要一个程序来模拟传统终端的行为，即 终端模拟器 (Terminal Emulator)。严格来讲，Terminal Emulator 的译名应该是「终端仿真器」。对于那些命令行 (CLI) 程序，终端模拟器会「假装」成一个传统终端设备；而对于现代的图形接口，终端模拟器会「假装」成一个 GUI 程序。一个终端模拟器的标准工作流程是这样的：捕获你的键盘输入；将输入发送给命令行程序（程序会认为这是从一个真正的终端设备输入的）；拿到命令行程序的输出结果（STDOUT 以及 STDERR）；调用图形接口（比如 X11），将输出结果渲染至显示器。终端模拟器有很多，这里就举几个经典的例子：GNU/Linux：gnome-terminal、Konsole； macOS：Terminal.app、iTerm2； Windows：Win32 控制台、ConEmu 等。在专门的终端硬件已经基本上仅存于计算机博物馆的现代，人们通常图省事儿，直接称呼终端模拟器为「终端」。2.3.TTY简单说，tty就是终端的总称。最早的 Unix 终端是 ASR-33 电传打字机。而电传打字机 (Teletype / Teletypewriter) 的英文缩写就是 tty，即 tty 这个名称的来源。由于 Unix 被设计为一个多用户操作系统，所以人们会在计算机上连接多个终端（在当时，这些终端全都是电传打字机）。Unix 系统为了支持这些电传打字机，就设计了名为 tty 的子系统（没错，因为当时的终端全都是 tty，所以这个系统也被命名为了 tty，就是这么简单粗暴），将具体的硬件设备抽象为操作系统内部位于 /dev/tty* 的设备文件。3. Shell —— 提供用户界面的程序大家都知道，操作系统有一个叫做 内核 (Kernel) 的东西，它管理着整台计算机的硬件，是现代操作系统中最基本的部分。但是，内核处于系统的底层，是不能让普通用户随意操作的，不然一个不小心系统就崩溃啦！但我们总还是要让用户操作系统的，怎么办呢？这就需要一个专门的程序，它接受用户输入的命令，然后帮我们与内核沟通，最后让内核完成我们的任务。这个提供用户界面的程序被叫做 Shell (壳层)。其实 Shell 只是提供了一个用户操作系统的入口，我们一般是通过 Shell 去调用其他各种各样的应用程序，最后来达成我们的目的。比如说我们想要知道一个文件的内容，我们会在 Shell 中输入命令 cat foo.txt，然后 Shell 会帮我们运行 cat 这个程序，cat 再去调用内核提供的 open 等系统调用来获取文件的内容。虽然并不是 Shell 直接去与内核交互，但广义上可以认为是 Shell 提供了与内核交互的用户界面。Shell 通常可以分为两种： 命令行 Shell 与 图形 Shell。顾名思义，前者提供一个命令行界面 (CLI)，后者提供一个图形用户界面 (GUI)。Windows 下的 explorer.exe 就是一个典型的图形 Shell（没错，它确实是，因为它接受来自你的指令，并且会帮你与内核交互完成你的指令）。常见或历史上知名的命令行 Shell 有： 适用于 Unix 及类 Unix 系统： sh (Bourne shell)，最经典的 Unix shell； bash (Bourne-Again shell)，目前绝大多数 Linux 发行版的默认 shell； zsh (Z shell)，我个人最喜欢的 shell； fish (Friendly interactive shell)，专注于易用性与友好用户体验的 shell； Windows 下的 cmd.exe (命令提示符) 与 PowerShell。还有其他各种五花八门的 Shell4.shell与终端的分工现在我们知道，终端干的活儿是从用户这里接收输入（键盘、鼠标等输入设备），扔给 Shell，然后把 Shell 返回的结果展示给用户（比如通过显示器）。而 Shell 干的活儿是从终端那里拿到用户输入的命令，解析后交给操作系统内核去执行，并把执行结果返回给终端。不过 Shell 与终端的分工有一些容易混淆的地方，这里以例子进行说明：终端将用户的键盘输入转换为控制序列（除了字符以外的按键，比如 左方向键 → ^[[D），Shell 则解析并执行收到的控制序列（比如 ^[[D → 将光标向左移动）；不过也有例外，比如终端在接收到 Ctrl + C 组合键时，不会把这个按键转发给当前的程序，而是会发送一个 SIGINT 信号（默认情况下，这会导致进程终止）。其他类似的特殊组合键有 Ctrl-Z 与 Ctrl- 等，可以通过 stty -a 命令查看当前终端的设置。Shell 发出类似「把前景色改为红色（控制序列为 033[31m）」「显示 foo」等指令；终端接收这些指令，并且照着 Shell 说的做，于是你就看到了终端上输出了一行红色的 foo。除非被重定向，否则 Shell 永远不会知道它所执行命令的输出结果。我们可以在终端窗口中上下翻页查看过去的输出内容，这完全是终端提供的 feature，与 Shell 没有半毛钱关系；命令提示符 (Prompt) 是一个完全的 Shell 概念，与终端无关；行编辑、输入历史与自动补全等功能是由 Shell 提供的（比如 fish 这个 Shell 就有着很好用的历史命令与命令自动补全功能）。不过终端也能自己实现这些功能，比如说 XShell 这个终端模拟器就可以在本地写完一行命令，然后整条发送给远程服务器中的 Shell（在连接状况不佳时很有用，不然打个字都要卡半天）；终端中的复制粘贴功能（Shift + Insert 或者鼠标右键等）基本上都是由终端提供的。举个例子，Windows 默认的终端对于复制粘贴的支持很屎，而换一个终端（例如 ConEmu）后就可以很好地支持复制粘贴。不过 Shell 以及其他命令行程序也可以提供自己的复制粘贴机制（例如 vim）。
使用iptables设定特定端口连接数网速（万能方法）
使用iptables设定特定端口连接数（万能方法）限制端口连接数量首先输入命令service iptables stop关闭iptables（注意：iptables可能会有问题，貌似在旧版本中不被认为是一个服务，而是防火墙，这个问题我还没有解决，如果你解决了请告诉我一声，谢谢）限制端口并发数很简单，IPTABLES就能搞定了，假设你要限制端口8388的IP最大连接数为5，两句话命令：iptables -I INPUT -p tcp --dport 8388 -m connlimit --connlimit-above 5 -j DROP iptables -I OUTPUT -p tcp --dport 8388 -m connlimit --connlimit-above 5 -j DROP我再举个例子，比如你想限制从1024-10240的端口iptables -I INPUT -p tcp --dport 1024:10240 -m connlimit --connlimit-above 5 -j DROP iptables -I OUTPUT -p tcp --dport 1024:10240 -m connlimit --connlimit-above 5 -j DROP保存IPTABLES规则即可（service iptables save），其他端口以此类推。输入命令service iptables start启动最后用命令查看是否生效iptables -L -n -v限制端口速度首先输入命令service iptables stop关闭iptables限制端口并发数很简单，IPTABLES就能搞定了，假设你要限制端口5037的最大连接速度为60个包每秒，两句话命令：iptables -A INPUT -p tcp --sport 5037 -m limit --limit 60/s -j ACCEPT iptables -A INPUT -p tcp --sport 5037 -j DROP也就是限制每秒接受60个包，一般来说每个包大小为64—1518字节(Byte)。限制指定ip的访问速度原理：每秒对特定端口进行速度控制，比如每秒超过10个的数据包直接DROP，从而限制特定端口的速度iptables -A FORWARD -m limit -d 208.8.14.53 --limit 700/s --limit-burst 100 -j ACCEPT iptables -A FORWARD -d 208.8.14.53 -j DROP最后说一下如何解决防火墙重启后失败的问题iptables-save >/etc/sysconfig/iptables echo 'iptables-restore /etc/sysconfig/iptables' >> /etc/rc.local chmod +x /etc/rc.d/rc.local对于ssr客户端限制设备连接数打开你的配置文件，假设你在 /root 文件夹中安装的ShadowsocksR服务端，那么就是：vi /root/shadowsocksr/user-config.json找到协议参数（参数为空 "" 时，默认限制 64个设备数）"protocol_param": "",在协议参数中设置你要限制 每个端口最大设备连接数（建议最少2个），比如 限制最大 5个设备同时链接，那么改为："protocol_param": "5",注意：协议参数仅在服务端 协议设置(protocol)为 非原版(origin)协议并不兼容原版(_compatible) 时才有效！限制端口速度还是上文的那个地方，第一个是单线程限制，另外一个是总限制"speed_limit_per_con": 0, "speed_limit_per_user": 0,当你使用这个端口 下载某个文件时，单线程下载限速 100KB/S ，多线程下载(比如5个线程)就是 500KB/S 了，Youtube是单线程。