-
PHP 문득 궁금한거 생길때 정리간단기법 2023. 5. 16. 06:13
코드를 재사용하는 방향성
<?php public function model($model){ require_once '../app/models/' . $model . '.php'; return new $model(); } ?>
PHP의 코드 재사용 방식은 다른 파일을 포함하여 이루어진다. 위 코드처럼 문자열 변수를 이용하여 PHP파일을 동적으로 포함하여 사용하는 방식이 쓰인다. PHP는 클래스를 생성하기 위해서 동적인 문자열을 사용할 수 있는 점을 활용한다.
위 코드가 구조적인 형상을 갖추기 위해서 오토로드를 사용하는 방법이 있다. spl_autoload_register 함수로 클래스를 가져오는 방법을 한 번 등록해 주면, 클래스를 불러올 때마다 명시적으로 require_once 같은 포함 구문을 사용하지 않아도 된다.
예를 들어, App\Modesl 디렉토리 안에 Post클래스가 있고, 루트 디렉토리에 index.php 파일이 있다고 해보자.
// index.php. 루트경로이다. <?php spl_autoload_register(function ($class) { require_once './' . $class . '.php'; }); use App\Models\Post; $post = new Post(); var_dump($post->title); ?> <?php // App\Models\Post.php. <?php namespace App\Models; class Post { public $title; public $content; public function __construct() { $this->title = 'my title'; $this->content = 'my contnet'; } }; ?>
Post클래스는 현재경로 아래에 App/Models 디렉토리 아래에 있다. 그리고 네임스페이스가 디렉토리 구조를 따라간다. 이 Post를 사용하기 위해서 use를 이용하여 클래스의 경로까지 명시를 해주게 되면, 이 클래스의 fully qualified name은 App\Models\Post가 된다. 이 클래스 이름이 spl_autoload_register의 콜백함수로 들어간다. 콜백함수 안에서 require_once가 호출되고, Post클래스를 사용할 수 있도록 포함하는 과정이 진행된다.
첫 번째 방식에 네임스페이스와 파일 경로를 동기화하고, 오토로드를 사용하여 구조화한 것이다. 클래스를 만들 때마다 require_once를 호출하고, 그 경로를 신경 쓰는 대신 한 번 구조화를 잘해두고 규칙을 정하면 그다음엔 신경 쓰지 않을 수 있다.
PHP는 인터프리터 기반 언어인가?
PHP는 인터프리터 기반 언어이다. PHP 실행기가 PHP 스크립트를 읽고 바로 실행하기 때문이다. 컴파일 기반 언어처럼 코드를 기계어나 바이트코드 같은 형태로 바꾸지 않는다.
동시에 PHP는 컴파일 언어 같은 특징을 가진다. PHP 스크립트를 PHP 엔진이 컴파일하여 만들어낸 바이트코드(opcode)를 OPCache에 저장하여 재사용하기 때문이다. 그리고, PHP8에서는 JIT 컴파일러를 이용하여 머신코드를 재사용할 수 있게 되었다.
그렇다면 컴파일 기반 언어라고 할 수 있나? 아무래도 거리가 있어 보인다. 컴파일 타임 언어라고 한다면 소스 코드를 컴파일러가 머신코드로 바로 생성하여 실행할 수 있게 하거나 바이트 코드로 만들어서 VM이 실행한다거나 해야 한다. OPcache를 사용하더라도, 최초의 스크립트는 PHP 인터프리터에 의하여 변환되고 그 결과가 재사용되는 것에 가깝다. 애초에 OPcache는 사용자가 명시적으로 활성화하지 않으면 사용할 수 없기도 하고.
아파치와 mod_php
아파치는 웹 서버이다. PHP는 서버사이드 스크립트 언어로 동적인 웹 페이지를 만들기 위해 나온 언어이다. 현재의 PHP는 빌트인 웹서버가 있지만, 과거엔 그렇지 않았다. 그래서 아파치 같은 웹서버와 함께 사용되었다.
아파치가 PHP를 실행하는 방법은 크게 CGI와 mod_php가 있다. CGI는 아파치가 PHP 프로세스를 생성하여 PHP인터프리터가 PHP 스크립트를 처리하도록 하는 방식이다. mod_php는 아파치 내부에서 PHP 스크립트를 처리한다.
mod_php를 사용하는 단계는 다음과 같다.
- 아파치 설치
- PHP 설치
- 아파치의 설정에서 mod_php를 활성화하고, PHP 연결
현재 PHP가 어떤 방식(CGI, mod_php ...)로 실행되는지 파악하는 방법은 다음과 같다.
- phpinfo를 출력해서 Server API를 확인한다
- 또는 php_sapi_name() 함수를 호출해서 확인할 수도 있다.
- 또는 아파치 설정 파일에서 LoadModule php... 를 확인해 볼 수도 있다.
왜 PHP는 텍스트를 바로 출력하는지?
PHP파일 안에 텍스트로 hello world라고 치고, 웹으로 접근해 보면 hello world를 반환해 준다. PHP 파일 안에 PHP코드가 없음에도 불구하고 그렇다. 왜 그런가. PHP는 서버에서 동적으로 html을 생성해 주기 위해 디자인된 언어이기 때문이다.
클라이언트(주로 웹 브라우저)가 PHP파일을 요청하면, 웹 서버는 PHP인터프리터를 실행하여 PHP코드를 통하여 HTML 코드를 생성해 낸다. HTML 코드는 다시 클라이언트로 응답된다. PHP인터프리터는 <?php ?>태그 밖에 있는 모든 내용들을 HTML 코드로 처리한다.
include의 동작 원리
PHP인터프리터는 include 문장을 만나면, 해당 파일을 열고 그 안의 모든 내용물을 읽어온다. 그리고 include 문장을 해당 파일의 내용물로 교체한다.
<?php echo "hello"; include('foo.php'); echo "world"; // <?php echo "hello"; ?> {CONTENT OF foo.php} <?php echo "world";
그런데, 다음과 같이 하나의 파일엔 strict_types을 적용하고, 그렇지 않은 파일에서 그 파일을 포함할 땐 어떻게 될까? include 문을 쓴 호출자의 설정을 우선하게 된다.
<?php // function.php (file) declare (strict_types = 1); function sum(int $a, int $b) { return $a + $b; } /** * This will throw TypeError: */ // echo sum(5.2, 5); Include does not behave like copy paste. Here is a demonstration using PHP Strict Type Declarations <?php // function.php (file) declare (strict_types = 1); function sum(int $a, int $b) { return $a + $b; } /** * This will throw TypeError: */ // echo sum(5.2, 5); However, if I call this function from an external file <?php // caller.php (file) require_once 'function.php'; /** * This will Work instead of throwing a TypeError: */ echo sum(5.2, 5);
PHP스크립트 안에서 return을 하면?
PHP스크립트 안에서 return을 하면 값을 가져올 수 있다.
$result = require_once 경로;
스크립트 안에서 return을 하면, 혹시 스크립트 안의 글로벌 변수들이 로컬 스코프로 변하지 않을까 싶었지만... 그렇진 않네. PHP의 포함방식은 스크립트 교체+@ 정도를 한 수준이었고, 리턴 값을 가져올 수 있지만, 스크립트 안의 글로벌 스코프 변수들도 따라온다.
PHP네임스페이스 적용 범위
- global scope의 변수엔 적용 안됨
- 클래스 적용
- 함수 적용
네임스페이스로 접근
- 클래스에 접근할 때, use Namespace\...\클래스이름
- 함수에 접근할 때, use function Namespace\...\함수이름
- 다른 이름을 줄 때, 위 예제의 마지막에 as 이름
- 상수에 접근할 떄, use const Namespace\...\상수이름
php.ini 파일 적용 순서
숫자가 낮을수록 우선순위가 낮다.
- php.ini 파일이 적용됨
- conf.d 디렉토리 안에 설정 파일이 추가로 있다면, php.ini 파일의 설정을 덮어씀
- PHPINIDir 을 적용한 디렉토리. 처음보지만, apache를 이용하여 설정할 수 있다.
- virtual host 설정
- .httaccess 파일
- 소스 코드 안에서 설정한다면, 가장 우선순위가 높다.
설치할 때, 왜 thread-safe 버전을 쓰는지?
PHP 스스로 웹 서버를 구동할 수 없었던 과거엔 아파치와 함께 쓰였다. 유저의 요청아 아파치를 거쳐 PHP인터프리터를 실행하고, 스크립트에서 출력된 결과가 아파치를 거쳐 유저에게 응답한다. 아파치엔 mod_php로 PHP자체가 모듈로 들어가 있다. 이 모듈을 구동하는 방식이 바로 thread-safe방식. 따라서 아파치를 기반으로 PHP를 구동할 땐, thread-safe 버전을 사용한다.
PHP 클래스에 쓰이는 3가지 키워드 self, parent, static
- self는 클래스의 스태틱 메소드, 프로퍼티에 접근할 수 있는 키워드이다. 스태틱이 아니라면 self로 접근할 수 없다. 논스태틱 메소드에서 self로 스태틱 멤버에 접근할 수 있다. 일종의 컴파일 타임에서 생성되는 스태틱 바인딩이라고 할 수 있다.
- static은 late binding(다이나믹 바인딩)을 위해서 self대신 쓸 수 있는 키워드이다.
- parent는 클래스의 상위 클래스에 접근할 수 있게 해주는 키워드이다. 스태틱 멤버엔 접근할 수 없다. 자식 클래스에서 상위 클래스에 접근할 일이 별로 없지만 construct 등에 쓰인다.
아래는 self와 static의 차이를 보여주는 코드이다.
<?php class Base { const CONSTANT = "base"; public static function PrintWithSelf() { echo self::CONSTANT; } public static function PrintWithStatic() { echo static::CONSTANT; } } class Derived extends Base { const CONSTANT = "Derived"; } $d = new Derived(); $d->PrintWithSelf(); // "base"; $d->PrintWithStatic(); // "Derived";
::class란?
::class는 어떤 클래스나 오브젝트의 뒤에서 쓰면, 네임스페이스를 포함한 클래스 이름을 반환해 준다. 그래서 클래스 안에 따로 클래스 이름을 보관할 필요가 없다. 그리고 클래스에서 쓰이는 static 키워드와 함께 쓰이면, 늦은 바인딩을 할 수 있게 해 준다. 다음 코드를 보자.
class A { public function getClassName(){ return __CLASS__; } public function getRealClassName() { return static::class; } } class B extends A {} $a = new A; $b = new B; echo $a->getClassName(); // A echo $a->getRealClassName(); // A echo $b->getClassName(); // A echo $b->getRealClassName(); // B
Scope Resolution Operator
"::"연산자는 클래스의 스태틱, 상수, 오버라이딩된 메소드에 접근할 수 있게 해 준다. 클래스의 이름을 사용하여 클래스 밖에서도 접근할 수 있다. 이때, 클래스 이름을 변수에 담아서 접근할 수도 있다. 꼭 self, parent, static 키워드를 앞에 붙이지 않아도 된다.
상속할 수 있는 싱글톤 코드
abstract class Singleton { protected function __construct() { } final public static function getInstance() { static $instances = array(); $calledClass = get_called_class(); // static::class도 가능 if (!isset($instances[$calledClass])) { $instances[$calledClass] = new $calledClass(); } return $instances[$calledClass]; } final private function __clone() { } } class FileService extends Singleton { // Lots of neat stuff in here } $fs = FileService::getInstance();
PHP의 바인딩
바인딩은 클래스가 호출할 메소드를 연결해 주는 과정이라 할 수 있다. 보통의 클래스는 클래스에서 선언된 메소드를 따라갈 것이다. 그런데 다형성을 가지게 되는 행위, 예를 들어 상속을 하고 오버라이딩을 한다면, 같은 코드이지만 다른 동작을 하게 된다.
이런 바인딩을 크게 정적 바인딩(컴파일 타임 바인딩), 동적 바인딩(런타임 바인딩)으로 나눌 수 있다. 정적 바인딩은 컴파일 시간에 클래스와 메소드가 연결되고 그것이 실행시간에도 유지된다. 동적 바인딩은 실행 시간에 다른 메소드를 호출할 수 있는 바인딩이다. 정적 바인딩은 다시 이른(early) 바인딩, 동적 바인딩은 늦은(late) 바인딩이라고 하기도 한다.
PHP의 바인딩은 LSB(Late Static Binding)이다. 문법적으로는 static::method() 처럼 생겼다. 우선 실행시간에 어떤 메소드를 호출할지 결정하므로 Late이고, 정적 메소드를 호출할 수 있어서 Static이란 이름이 붙었다. 이전에 구별했던 스태틱, 동적 바인딩에서 스태틱은 스태틱 메소드를 말하는 게 아니라 어떤 고정된 상태를 의미하는 것인데, 여기선 정적 메소드도 바인딩한다는 의미에서 스태틱이 사용되었다.
Zend engine은 무엇인가?
PHP를 실행하는 런타임이다. PHP 스크립트를 읽고 실행하는 것이 주된 역할. 이를 위해 메모리 관리를 하거나 PHP에 사용되는 배열, 문자열 등을 위한 자료구조를 생성한다. PHP 스크립를 컴파일(JIT)하고 최적화한다. 그밖에 OPcache 같은 익스텐션을 제공하기도 한다.
매직 메소드
try-cath에서 빠져나오기
PHP에서 사용하는 try-catch 문의 중간에서 빠져나오기
<?php do { try { // DB에 접근 하는 등의 코드를 try-catch문 안에서 쓴다. // validation이 실패하여 더 이상 try-catch문을 진행하지 않는다. if($result !== ERROR_OK) { break; } // 위 break가 실행되면 아래 코드는 진행안됨 ...code... } catch(Exception $e) { ...예외... } } while(false); // do-while문의 if 안에서 break를 걸면 do-while을 빠져나온다. ?> 위 코드를 간결하게 하려면 사실 goto를 쓰면 되긴 하다. 그런데, goto는 일단 쓰게 되면 코드 파악을 어렵게 한다. 최소한 위에서 아래로 가야하며, 아래에서 위로 가는 것을 금지하는 정도의 제약을 가해야하지만 이런 제약을 지키기 어렵다면 문법적으로 do-while을 활용할 수 있다.
prepare statement 호출 결과
<?php /* query가 SELECT... 인데 매칭되는 row가 0이라면 $stmt->prepare, bind_param, execute, get_result()까지 호출하면 그 결과로 mysqli_result가 나오되 num_row는 0으로 나온다. 이 상태에서 다시 fetch_assoc()류의 결과 반환 함수를 쓰면, 그 결과는 NULL이 된다. */ ?>
출력 버퍼링(Output buffering)
PHP에서 출력 버퍼링이 없다면, PHP 스크립트가 실행되면 출력(echo, print, <html>...)되는 것들 마다 패킷이 생성되어 웹서버에 전달된다. 출력 버퍼링을 활성화하면, 출력을 모아 스크립트가 종료되면 하나의 청크로 만들어 한 번에 보낸다. 이론상으로 효율적인 부분이 있다.
브라우저에 출력이 바로 되지 않는 이유
그런데, 실제로 브라우저에서 확인해 보면, 출력 버퍼링을 껐음에도 불구하고 출력한 내용이 바로바로 브라우저에 나타나지 않는다. 출력버퍼링을 켜고 ob_flush류의 함수를 호출해도 동일하다. 그 이유는 브라우저 자체적으로 페이지가 모두 로드될 때까지 업데이를 하지 않기 때문이다.
Headers already sent 에러
PHP의 HTTP 요청-응답 구조는 클라이언트 -> 웹서버 -> PHP스크립트 -> 웹서버 -> 클라이언트이다. HTTP는 헤더와 바디로 나누어져 있고, 그 사이에 빈 공간이 있다. PHP스크립트에서 어떤 컨텐츠를 출력하면 그것이 바디로 간다. HTTP메시지는 헤더도 필요하므로 헤더 역시 같이 보내진다. 이렇게 한 번 헤더와 바디 구조가 잡히면, 다시 헤더를 바꿀 수 없는 구조를 가지고 있다. 출력에는 print, echo와 같은 PHP 코드와 PHP태그 밖에 있는 HTML코드이다. 이것을 실행하는 순간 헤더와 함께 출력된다. 이후에는 헤더를 추가하는 것이 금지된다.
이 이슈가 발생하는 경우는 꽤 많다. <?PHP 태그 앞에 빈 공간이 있다던지, include하는 파일 안에서 출력을 한다던지, HTML파일이 중간중간에 있다던지 등등. 때문에 마지막 순간에만 출력하는 규칙을 세우는 것으로는 해결이 안 된다.
잘 알려진 해결 방법은 출력 버퍼를 설정하는 것이다. output_buffering=... 세팅을 추가하면, PHP는 웹서버에 바로 응답을 보내지 않고, 출력 버퍼에 모아두고 한 번에 보낸다. 출력 버퍼링의 목적은 너무 작은 데이터를 여러 번 보내지 말고 청크로 묶어서 보내는 것이지만, Headers already sent 에러를 해결할 수 있다.
함수 오버로딩
PHP에는 함수 오버로딩이 없다. 같은 이름의 함수인데 함수 인자나 리턴 타입이 다른 그런 종류의 오버로딩은 없다. 그런데 PHP에서는 오버로딩이라는 이름으로 하나의 기능을 제공한다. 바로 클래스 안에서 멤버 변수의 값을 정하거나 가져오는 것을 후킹 하도록 __set, __get이라는 함수를 제공한다. 그리고 저 함수를 정의해서 사용하는 것으로 오버로딩이라고 한다.
prepared statement가 제대로 적용되었는지 확인
mysql의 설정 파일인 my.cnf에 general log를 활성화하고 로그를 남긴다.
[mysqld] general_log = ON general_log_file= 경로/general.log
로그를 보면 다음과 같이 나오는 것을 볼 수 있다.
배열
PHP 배열은 ordered map이고 그 구현의 바탕은 해쉬 테이블이다. 그래서 key로 접근하는 시간 복잡도는 O(1), 맨 끝에 추가하는 것은 O(1), 앞에 추가하는 것은 재조정이 필요하여 O(N), 삭제하는 것은 O(1), 루핑하며 특정 데이터를 찾는 것은 O(N)이다. 배열이면서 맵이기에 그래서 array(1, 2, 3, 10 => 30, 'a' => 'b'); 같은 생성방법을 허용한다. key로는 정수와 문자열을 받는다. 만약 문자열 '1'과 1을 키로 넣는다면 구별할 수 없다. $arr[1]이나 $arr['1']이나 같이 적용된다. 마지막에 들어온 것이 선택된다.
생성
// 배열 기본 array(1, 2, 3) array(0 => 1, 1 => 2, 2 => 3); array('a' => 'b', 'c' => 'd'); array 대신 []를 써도 된다. //만약 특정 길이를 가진 배열을 만들고 싶다면? array_fill(0, length, value); // 0은 시작 인덱스이다. //만약 특정 길이를 지닌 N*M차원 배열을 만들고 싶다면? array_fill(0, N, array_fill(0, M, 0));
그런데 2차원 배열을 만들기 위해 배열을 함수의 인자로 넘긴다면 모든 row에서 하나의 배열을 보고 있지 않을까?라는 의심이 들기 마련이다. 하지만, PHP의 배열은 함수로 넘겨질 때, 값 복사로 넘겨지므로 괜찮다. 역시 저 저렇게 만든 2차원 배열을 넘겨서 3차원 배열을 만들더라도 값복사로 넘겨져서 괜찮다.
그러나 만약에, 배열 안에 있는 값이 오브젝트라면, 배열 자체는 값으로 넘겨지더라도 오브젝트를 참조하고 있으므로 문제가 생긴다.
class foo { public $a = 10; } $bar = new foo(); $arr_2d = array_fill(0, 2, array_fill(0, 2, $bar)); $arr_3d = array_fill(0, 2, $arr_2d); $arr_3d[0][0][0]->a = 999; // 모든 엘리먼트는 foo->$a = 999가 된다.
삭제
배열에서 키를 이용하여 하나의 원소를 삭제한다면? unset 혹은 array_splice
// unset $array = [0 => "a", 1 => "b", 2 => "c"]; unset($array[1]); [ [0] => a [2] => c ] $array = array_values($array); [ [0] => a [1] => c ] // array_splice $array = [0 => "a", 1 => "b", 2 => "c"]; \array_splice($array, 1, 1); [ [0] => a [1] => c ] unset과 array_splice의 차이점은 제거후에 인덱스 재조정을 하느냐 안하느냐. 만약 unset 이후에 재조정을 원한다면 array_values를 호출하는 방법이 있다.
배열에서 다수의 엘리먼트를 삭제한다면? array_diff(원본배열, 삭제할원소), array_diff_key(원본배열,삭제할원소키), 혹은 array_filter를 이용할 수도 있다.
stdClass
stdClass는 동적인 프로퍼티를 가질 수 있는 제네릭한 클래스다. object타입으로 타입캐스팅을 위해 준비한 클래스로 보인다.
// 배열 -> object $obj = (object) array('foo' => 'bar'); var_dump($obj); // 결과 object(stdClass)#1 (1) { ["foo"]=> string(3) "bar" } // sclar -> object $val = 90; $obj = (object)$val; var_dump($obj); var_dump($obj->scalar); // 결과 object(stdClass)#1 (1) { ["scalar"]=> int(90) } int(90)
위 코드를 보면 알 수 있듯 배열 -> 오브젝트, 스칼라(int, float, bool, string) -> object로 변환을 가능하게 해 준다. 이 외에도 json_decode의 결과 타입이 된다. json_decode로 생성되는 object의 key,value는 동적이니 적절해 보인다.
// Created as a result of json_decode() <?php $json = '{"foo":"bar"}'; var_dump(json_decode($json)); // 결과 object(stdClass)#1 (1) { ["foo"]=> string(3) "bar" }
클로저을 제공하는 방법이 있는지?
있다. 익명함수에 use 키워드를 사용하면 된다.
$a = 1; $f = function() use ($a) { echo $a; }; $a = 2; $f(); // 출력이 1이다.
하지만 위와 같이 사용한다면, 값을 그대로 복사하므로 캡쳐의 느낌이 강하다. 만약 $a의 변화를 익명 함수에도 퍼뜨리려면 어떻게 해야 하나? 레퍼런스를 넘기면 된다.
$a = 1; $f = function() use (&$a) { echo $a; }; $a = 2; $f(); // 2 출력
익명함수 재귀
php 익명함수의 코드는 다음과 같다.
$func = function ($arg1, $arg2) use($arg3) { // 코드 }; $func();
익명함수를 재귀로 사용하려면 어떻게 해야할까? 익명함수를 담은 변수를 캡처하되 레퍼런스로 캡처해야 한다.
$func = function ($arg1, $arg2) use(&$func) { // 코드 }; $func();
왜 그럴까? use(&$func)이 호출되는 그 순간에 $func는 null이므로, 값으로 캡처하면 null이 넘어온다. 따라서 레퍼런스로 해줘야 한다.
참고
'간단기법' 카테고리의 다른 글
PHP 에러 핸들링 (0) 2023.05.24 에러 핸들링, PHP, mysqli, prepared statement (0) 2023.05.17 동적인 데이터를 처리하는 다양한 방법. (0) 2023.03.13 매크로 expand(STRING_CAT 매크로를 예시로) (0) 2022.10.05 문자열 뒤집기 두 가지 버전 (0) 2022.09.17