버그가 있는 PHP 코드: PHP 개발자가 저지르는 가장 일반적인 실수 10가지
게시 됨: 2022-03-11PHP를 사용하면 웹 기반 시스템을 비교적 쉽게 구축할 수 있으며 이것이 인기를 얻는 이유입니다. 그러나 사용의 용이성에도 불구하고 PHP는 개발자를 물릴 수 있는 많은 프레임워크, 뉘앙스 및 미묘함을 가진 상당히 정교한 언어로 발전하여 몇 시간 동안 디버깅을 해야 했습니다. 이 기사에서는 PHP 개발자가 주의해야 하는 일반적인 실수 10가지를 강조합니다.
일반적인 실수 #1: foreach
루프 이후에 댕글링 배열 참조를 남겨두기
PHP에서 foreach 루프를 사용하는 방법을 모르십니까? 반복하는 배열의 각 요소에 대해 작업하려는 경우 foreach
루프에서 참조를 사용하는 것이 유용할 수 있습니다. 예를 들어:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8)
문제는 주의하지 않으면 바람직하지 않은 부작용과 결과를 초래할 수 있다는 것입니다. 특히 위의 예에서 코드가 실행된 후 $value
는 범위에 남아 있고 배열의 마지막 요소에 대한 참조를 보유합니다. 따라서 $value
와 관련된 후속 작업은 의도하지 않게 배열의 마지막 요소를 수정하게 될 수 있습니다.
기억해야 할 주요 사항은 foreach
가 범위를 생성하지 않는다는 것입니다. 따라서 위의 예에서 $value
는 스크립트의 최상위 범위 내의 참조 입니다. 각 반복에서 foreach
는 $array
의 다음 요소를 가리키도록 참조를 설정합니다. 따라서 루프가 완료된 후에도 $value
는 여전히 $array
의 마지막 요소를 가리키며 범위에 남아 있습니다.
다음은 이로 인해 발생할 수 있는 회피적이고 혼란스러운 버그의 예입니다.
$array = [1, 2, 3]; echo implode(',', $array), "\n"; foreach ($array as &$value) {} // by reference echo implode(',', $array), "\n"; foreach ($array as $value) {} // by value (ie, copy) echo implode(',', $array), "\n";
위의 코드는 다음을 출력합니다.
1,2,3 1,2,3 1,2,2
아니요, 오타가 아닙니다. 마지막 줄의 마지막 값은 실제로 3이 아니라 2입니다.
왜요?
첫 번째 foreach
루프를 거친 후 $array
는 변경되지 않은 상태로 유지되지만 위에서 설명한 것처럼 $value
는 $array
의 마지막 요소에 대한 댕글링 참조로 남습니다( foreach
루프가 참조 로 $value
에 액세스했기 때문에).
결과적으로 두 번째 foreach
루프를 거치면 "이상한 일"이 발생하는 것처럼 보입니다. 특히, $value
는 이제 값으로(즉, copy 로) 액세스되므로 foreach
는 루프의 각 단계에서 각 순차적 $array
요소를 $value
로 복사 합니다. 결과적으로 다음은 두 번째 foreach
루프의 각 단계에서 발생하는 일입니다.
- 전달 1:
$array[0]
(즉, "1")을$value
($array[2]
에 대한 참조)에 복사하므로$array[2]
는 이제 1과 같습니다. 따라서$array
에는 이제 [1, 2, 1]. - 전달 2:
$array[1]
(즉, "2")를$value
($array[2]
에 대한 참조)에 복사하므로$array[2]
는 이제 2와 같습니다. 따라서$array
에는 이제 [1, 2, 2]. - 패스 3:
$array[2]
(이제 "2"와 같음)를$value
($array[2]
에 대한 참조임)에 복사하므로$array[2]
는 여전히 2와 같습니다. 따라서$array
는 이제 [1 , 2, 2].
이러한 종류의 문제에 대한 위험을 실행하지 않고 foreach
루프에서 참조를 사용하는 이점을 계속 얻으려면 foreach
루프 직후에 변수에 대해 unset()
을 호출하여 참조를 제거하십시오. 예:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value no longer references $arr[3]
일반적인 실수 #2: isset()
동작에 대한 오해
이름에도 불구하고 isset()
은 항목이 존재하지 않으면 false를 반환할 뿐만 아니라 null
값에 대해서도 false
를 반환합니다 .
이 동작은 처음에 나타날 수 있는 것보다 더 문제가 많으며 문제의 일반적인 원인입니다.
다음을 고려하세요:
$data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }
이 코드의 작성자는 아마도 keyShouldBeSet
이 $data
에 설정되었는지 확인하고 싶었을 것입니다. 그러나 논의한 바와 같이 isset($data['keyShouldBeSet'])
은 $data['keyShouldBeSet']
가 설정되었지만 null
로 설정된 경우 에도 false를 반환합니다. 따라서 위의 논리에는 결함이 있습니다.
다음은 또 다른 예입니다.
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; }
위의 코드는 $_POST['active']
가 true
를 반환하면 postData
가 반드시 설정되어 isset($postData)
가 true
를 반환한다고 가정합니다. 따라서 반대로 위의 코드는 isset($postData)
가 false
를 반환하는 유일한 방법은 $_POST['active']
가 false
를 반환하는 경우뿐이라고 가정합니다.
아니다.
설명된 대로 isset($postData)
는 $postData
가 null
로 설정된 경우에도 false
를 반환합니다. 따라서 $_POST['active']
가 true
를 반환하더라도 isset($postData)
가 false
를 반환할 수 있습니다. 다시 말하지만, 위의 논리에는 결함이 있습니다.
그런데 부수적으로, 위 코드의 의도가 실제로 $_POST['active']
가 true를 반환했는지 다시 확인하는 것이라면, 이에 대해 isset()
에 의존하는 것은 어떤 경우에도 잘못된 코딩 결정이었습니다. 대신 $_POST['active']
를 다시 확인하는 것이 더 나았을 것입니다. 즉:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; }
그러나 변수가 실제로 설정되었는지 확인하는 것이 중요한 경우(즉, 설정되지 않은 변수와 null
로 설정된 변수를 구별하기 위해) array_key_exists()
메서드가 훨씬 더 강력합니다. 해결책.
예를 들어 위의 두 가지 예 중 첫 번째 예를 다음과 같이 다시 작성할 수 있습니다.
$data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // do this if 'keyShouldBeSet' isn't set }
또한 array_key_exists()
와 get_defined_vars()
를 결합하여 현재 범위 내의 변수가 설정되었는지 여부를 안정적으로 확인할 수 있습니다.
if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }
일반적인 실수 #3: 참조로 반환하는 것과 값으로 반환하는 것에 대한 혼동
다음 코드 조각을 고려하십시오.
class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
위의 코드를 실행하면 다음과 같은 결과를 얻을 수 있습니다.
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
뭐가 문제 야?
문제는 위의 코드가 참조로 배열을 반환하는 것과 값으로 배열을 반환하는 것을 혼동한다는 것입니다. PHP에 명시적으로 참조로 배열을 반환하도록 지시하지 않는 한(즉, &
사용), PHP는 기본적으로 "값으로" 배열을 반환합니다. 이것은 배열의 복사본 이 반환되고 따라서 호출된 함수와 호출자가 배열의 동일한 인스턴스에 액세스하지 않는다는 것을 의미합니다.
따라서 위의 getValues()
호출은 참조가 아닌 $values
배열의 복사본 을 반환합니다. 이를 염두에 두고 위의 예에서 두 가지 핵심 라인을 다시 살펴보겠습니다.
// getValues() returns a COPY of the $values array, so this adds a 'test' element // to a COPY of the $values array, but not to the $values array itself. $config->getValues()['test'] = 'test'; // getValues() again returns ANOTHER COPY of the $values array, and THIS copy doesn't // contain a 'test' element (which is why we get the "undefined index" message). echo $config->getValues()['test'];
한 가지 가능한 수정은 getValues()
에서 반환된 $values
배열의 첫 번째 복사본을 저장한 다음 해당 복사본에 대해 작업하는 것입니다. 예:
$vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];
해당 코드는 잘 작동하지만(즉, "정의되지 않은 인덱스" 메시지를 생성하지 않고 test
를 출력함) 달성하려는 것에 따라 이 접근 방식이 적절할 수도 있고 적절하지 않을 수도 있습니다. 특히 위의 코드는 원래 $values
배열을 수정하지 않습니다. 따라서 수정 사항(예: 'test' 요소 추가) 이 원래 배열에 영향을 미치도록 하려면 대신 $values
배열 자체에 대한 참조 를 반환하도록 getValues()
함수를 수정해야 합니다. 이것은 함수 이름 앞에 &
를 추가하여 수행되므로 참조를 반환해야 함을 나타냅니다. 즉:
class Config { private $values = []; // return a REFERENCE to the actual $values array public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
이것의 출력은 예상대로 test
가 될 것입니다.
그러나 상황을 더 혼란스럽게 하려면 대신 다음 코드 스니펫을 고려하세요.
class Config { private $values; // using ArrayObject rather than array public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
이전 array
예제와 동일한 "정의되지 않은 인덱스" 오류가 발생할 것이라고 추측했다면 오산입니다. 사실, 이 코드는 잘 작동할 것입니다. 그 이유는 배열과 달리 PHP는 항상 참조로 객체를 전달하기 때문입니다. ( ArrayObject
는 배열 사용법을 완전히 모방하지만 객체로 작동하는 SPL 객체입니다.)
이 예제에서 알 수 있듯이 PHP에서 사본을 처리하는지 참조를 처리하는지 여부가 항상 완전히 명확한 것은 아닙니다. 따라서 이러한 기본 동작(즉, 변수와 배열은 값으로 전달되고 객체는 참조로 전달됨)을 이해하고 호출하는 함수에 대한 API 문서를 주의 깊게 확인하여 값을 반환하는지 확인하는 것이 중요합니다. 배열의 복사본, 배열에 대한 참조 또는 개체에 대한 참조.
즉, 배열 또는 ArrayObject
에 대한 참조를 반환하는 관행은 호출자에게 인스턴스의 개인 데이터를 수정할 수 있는 기능을 제공하기 때문에 일반적으로 피해야 하는 것입니다. 이것은 캡슐화의 "얼굴에 날아갑니다". 대신 다음과 같이 이전 스타일의 "getters" 및 "setter"를 사용하는 것이 좋습니다.
class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue('testKey', 'testValue'); echo $config->getValue('testKey'); // echos 'testValue'
이 접근 방식은 호출자에게 비공개 $values
배열 자체에 대한 공개 액세스를 제공하지 않고 배열의 값을 설정하거나 가져올 수 있는 기능을 제공합니다.
일반적인 실수 #4: 루프에서 쿼리 수행
PHP가 작동하지 않는 경우 다음과 같은 문제가 발생하는 것은 드문 일이 아닙니다.
$models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }
여기에는 전혀 문제가 없을 수 있지만 코드의 논리를 따르면 위의 $valueRepository->findByValue()
에 대한 순진해 보이는 호출이 궁극적으로 다음과 같은 일종의 쿼리를 생성한다는 것을 알 수 있습니다.
$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
결과적으로 위의 루프를 반복할 때마다 데이터베이스에 대한 별도의 쿼리가 발생합니다. 따라서 예를 들어 루프에 1,000개의 값 배열을 제공하면 리소스에 대해 1,000개의 개별 쿼리가 생성됩니다! 이러한 스크립트가 여러 스레드에서 호출되면 잠재적으로 시스템이 중단될 수 있습니다.
따라서 코드에서 쿼리가 생성되는 시점을 인식하고 가능하면 값을 수집한 다음 하나의 쿼리를 실행하여 모든 결과를 가져오는 것이 중요합니다.
비효율적으로(즉, 루프에서) 쿼리가 수행되는 매우 일반적인 장소의 한 예는 양식이 값 목록(예: ID)과 함께 게시되는 경우입니다. 그런 다음 각 ID에 대한 전체 레코드 데이터를 검색하기 위해 코드는 배열을 반복하고 각 ID에 대해 별도의 SQL 쿼리를 수행합니다. 이것은 종종 다음과 같이 보일 것입니다:
$data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); }
그러나 다음과 같이 단일 SQL 쿼리에서 동일한 작업을 훨씬 더 효율적으로 수행할 수 있습니다.
$data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }
따라서 코드에서 직접 또는 간접적으로 쿼리가 수행되는 시기를 인식하는 것이 중요합니다. 가능하면 값을 수집한 다음 하나의 쿼리를 실행하여 모든 결과를 가져옵니다. 그러나 거기에서도 주의를 기울여야 하며, 이는 우리를 다음에 흔히 발생하는 PHP 실수로 이끕니다...
일반적인 실수 #5: 메모리 사용 헤드페이크 및 비효율성
한 번에 많은 레코드를 가져오는 것이 가져올 각 행에 대해 단일 쿼리를 실행하는 것보다 확실히 더 효율적이지만 이러한 접근 방식은 PHP의 mysql
확장을 사용할 때 libmysqlclient
에서 잠재적으로 "메모리 부족" 상태로 이어질 수 있습니다.
시연을 위해 제한된 리소스(512MB RAM), MySQL 및 php-cli
가 있는 테스트 상자를 살펴보겠습니다.
다음과 같이 데이터베이스 테이블을 부트스트랩합니다.
// connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // create table of 400 columns $query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT'; for ($col = 0; $col < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ');'; $connection->query($query); // write 2 million rows for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ', ' . mt_rand(1000000000, 9999999999); } $query .= ')'; $connection->query($query); }
자, 이제 리소스 사용량을 확인하겠습니다.

// connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo "Before: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo "Limit 1: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo "Limit 10000: " . memory_get_peak_usage() . "\n";
산출:
Before: 224704 Limit 1: 224704 Limit 10000: 224704
멋있는. 쿼리가 리소스 측면에서 내부적으로 안전하게 관리되는 것 같습니다.
하지만 확실히 하기 위해 한도를 한 번 더 높이고 100,000으로 설정해 보겠습니다. 어 오. 그렇게 하면 다음을 얻습니다.
PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11
무슨 일이에요?
여기서 문제는 PHP의 mysql
모듈이 작동하는 방식입니다. 실제로는 더러운 작업을 수행하는 libmysqlclient
에 대한 프록시일 뿐입니다. 데이터의 일부가 선택되면 메모리로 직접 이동합니다. 이 메모리는 PHP의 관리자가 관리하지 않기 때문에 memory_get_peak_usage()
는 쿼리의 한계를 높여도 리소스 사용률이 증가하지 않습니다. 이것은 우리가 메모리 관리가 괜찮다고 생각하는 안주에 속아 위에서 설명한 것과 같은 문제로 이어집니다. 그러나 실제로 우리의 메모리 관리에는 심각한 결함이 있으며 위와 같은 문제가 발생할 수 있습니다.
mysqlnd
모듈을 대신 사용하여 최소한 위의 가짜를 피할 수 있습니다(그 자체로 메모리 사용률이 향상되지는 않지만). mysqlnd
는 기본 PHP 확장으로 컴파일되며 PHP의 메모리 관리자 를 사용합니다.
따라서 mysql
이 아닌 mysqlnd
를 사용하여 위의 테스트를 실행하면 메모리 사용에 대한 훨씬 더 현실적인 그림을 얻을 수 있습니다.
Before: 232048 Limit 1: 324952 Limit 10000: 32572912
그건 그렇고, 그보다 더 나쁩니다. PHP 문서에 따르면 mysql
은 데이터를 저장하기 위해 mysqlnd
보다 두 배 많은 리소스를 사용하므로 mysql
을 사용하는 원래 스크립트는 실제로 여기에 표시된 것보다 훨씬 더 많은 메모리를 사용했습니다(대략 두 배).
이러한 문제를 방지하려면 쿼리 크기를 제한하고 반복 횟수가 적은 루프를 사용하는 것이 좋습니다. 예:
$totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); }
이 PHP 실수와 위의 실수 #4를 모두 고려할 때, 한편으로는 쿼리가 너무 세분화되고 반복되는 것과 각각의 쿼리가 있는 것 사이에서 코드가 이상적으로 달성해야 하는 건전한 균형이 있다는 것을 깨달았습니다. 개별 쿼리가 너무 큽니다. 삶의 대부분의 것들과 마찬가지로 균형이 필요합니다. 어느 쪽이든 좋지 않으며 PHP가 제대로 작동하지 않는 문제를 일으킬 수 있습니다.
일반적인 실수 #6: 유니코드/UTF-8 문제 무시
어떤 의미에서 이것은 PHP를 디버깅하는 동안 발생하는 문제보다 실제로 PHP 자체의 문제이지만 적절하게 해결된 적이 없습니다. PHP 6의 핵심은 유니코드를 인식하도록 만들었지만 2010년 PHP 6 개발이 중단되면서 보류되었습니다.
그러나 그렇다고 해서 개발자가 UTF-8을 적절하게 처리하고 모든 문자열이 반드시 "일반 ASCII"일 것이라는 잘못된 가정을 피하는 것은 결코 아닙니다. ASCII가 아닌 문자열을 제대로 처리하지 못하는 코드는 코드에 심한 heisenbug를 도입하는 것으로 유명합니다. "Schrodinger"와 같은 성을 가진 누군가가 시스템에 등록하려고 하면 간단한 strlen($_POST['name'])
호출도 문제를 일으킬 수 있습니다.
다음은 코드에서 이러한 문제를 피하기 위한 작은 체크리스트입니다.
- 유니코드와 UTF-8에 대해 잘 모른다면 최소한 기본 사항은 배워야 합니다. 여기에 훌륭한 입문서가 있습니다.
- 항상 이전 문자열 함수 대신
mb_*
함수를 사용하십시오(PHP 빌드에 "멀티바이트" 확장이 포함되어 있는지 확인하십시오). - 데이터베이스와 테이블이 유니코드를 사용하도록 설정되어 있는지 확인하십시오(많은 MySQL 빌드는 기본적으로 여전히
latin1
을 사용합니다). -
json_encode()
는 ASCII가 아닌 기호를 변환하지만(예: "Schrodinger"는 "Schr\u00f6dinger"가 됨)serialize()
는 변환하지 않습니다 . - 하드코딩되거나 구성된 문자열 상수와 문자열을 연결할 때 충돌을 피하기 위해 PHP 코드 파일도 UTF-8로 인코딩되었는지 확인하십시오.
이와 관련하여 특히 귀중한 리소스는 이 블로그의 Francisco Claria가 게시한 PHP 및 MySQL용 UTF-8 입문서입니다.
일반적인 실수 #7: $_POST
에 항상 POST 데이터가 포함되어 있다고 가정합니다.
이름에도 불구하고 $_POST
배열에는 항상 POST 데이터가 포함되지 않으며 비어 있는 것을 쉽게 찾을 수 있습니다. 이를 이해하기 위해 예를 살펴보겠습니다. 다음과 같이 jQuery.ajax()
호출을 사용하여 서버 요청을 한다고 가정합니다.
// js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' });
(참고로 여기에서 contentType: 'application/json'
을 참고하세요. API에서 많이 사용되는 JSON으로 데이터를 보냅니다. 예를 들어 AngularJS $http
서비스에 게시할 때 기본값입니다.)
이 예제의 서버 측에서는 단순히 $_POST
배열을 덤프합니다.
// php var_dump($_POST);
놀랍게도 결과는 다음과 같습니다.
array(0) { }
왜요? JSON 문자열 {a: 'a', b: 'b'}
어떻게 되었나요?
대답은 PHP가 콘텐츠 유형이 application/x-www-form-urlencoded
또는 multipart/form-data
경우에만 POST 페이로드를 자동으로 구문 분석 한다는 것입니다. 그 이유는 역사적입니다. 이 두 콘텐츠 유형은 본질적으로 몇 년 전에 PHP의 $_POST
가 구현되었을 때 사용된 유일한 유형이었습니다. 따라서 다른 콘텐츠 유형( application/json
과 같이 오늘날 매우 인기 있는 콘텐츠 유형 포함)의 경우 PHP는 자동으로 POST 페이로드를 로드하지 않습니다.
$_POST
는 슈퍼글로벌이기 때문에 한 번 재정의하면(가능하면 스크립트 초기에) 수정된 값(예: POST 페이로드 포함)은 코드 전체에서 참조할 수 있습니다. $_POST
는 PHP 프레임워크와 거의 모든 사용자 정의 스크립트에서 요청 데이터를 추출하고 변환하는 데 일반적으로 사용되기 때문에 이것은 중요합니다.
따라서 예를 들어 콘텐츠 유형이 application/json
인 POST 페이로드를 처리할 때 다음과 같이 요청 콘텐츠를 수동으로 구문 분석(즉, JSON 데이터 디코딩)하고 $_POST
변수를 재정의해야 합니다.
// php $_POST = json_decode(file_get_contents('php://input'), true);
그런 다음 $_POST
배열을 덤프할 때 POST 페이로드가 올바르게 포함되어 있음을 알 수 있습니다. 예:
array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }
일반적인 실수 #8: PHP가 문자 데이터 유형을 지원한다고 생각
이 샘플 코드를 보고 무엇을 인쇄할지 추측해 보십시오.
for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; }
'a'부터 'z'까지 답했다면 틀렸다는 것을 알고 놀랄 수 있습니다.
예, 'a'에서 'z'까지 를 인쇄하지만 'aa'에서 'yz'까지 인쇄합니다. 이유를 봅시다.
PHP에는 char
데이터 유형이 없습니다. string
만 사용할 수 있습니다. 이를 염두에 두고 PHP에서 string
z
를 증가시키면 aa
가 생성됩니다.
php> $c = 'z'; echo ++$c . "\n"; aa
그러나 문제를 더 혼란스럽게 하기 위해 aa
는 사전식으로 z
보다 작 습니다.
php> var_export((boolean)('aa' < 'z')) . "\n"; true
그렇기 때문에 위에 제시된 샘플 코드는 문자 a
~ z
를 인쇄한 다음 aa
~ yz
도 인쇄합니다. za
에 도달하면 멈춥니다. 이 값은 z
가 "보다 큼" 만나는 첫 번째 값입니다.
php> var_export((boolean)('za' < 'z')) . "\n"; false
이 경우 PHP에서 'a'부터 'z'까지 값을 적절하게 반복하는 한 가지 방법이 있습니다.
for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; }
또는 대안:
$letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; }
일반적인 실수 #9: 코딩 표준 무시
코딩 표준을 무시한다고 해서 직접 PHP 코드를 디버그해야 하는 것은 아니지만, 여기에서 논의해야 할 가장 중요한 사항 중 하나일 것입니다.
코딩 표준을 무시하면 프로젝트에 많은 문제가 발생할 수 있습니다. 기껏해야 일관성이 없는 코드가 생성됩니다(모든 개발자가 "자신의 일을 하기" 때문에). 그러나 최악의 경우 작동하지 않거나 탐색이 어려울 수 있는(때로는 거의 불가능) PHP 코드를 생성하여 디버그, 향상, 유지 관리를 극도로 어렵게 만듭니다. 이는 많은 낭비(또는 최소한 불필요한) 노력을 포함하여 팀의 생산성 감소를 의미합니다.
다행히 PHP 개발자에게는 다음 5가지 표준으로 구성된 PHP PSR(표준 권장 사항)이 있습니다.
- PSR-0: 자동 로딩 표준
- PSR-1: 기본 코딩 표준
- PSR-2: 코딩 스타일 가이드
- PSR-3: 로거 인터페이스
- PSR-4: 오토로더
PSR은 원래 시장에서 가장 인정받는 플랫폼의 유지 관리자의 의견을 기반으로 만들어졌습니다. Zend, Drupal, Symfony, Joomla 등이 이러한 표준에 기여했으며 현재 이를 따르고 있습니다. 몇 년 전부터 표준을 시도했던 PEAR도 지금은 PSR에 참여하고 있다.
어떤 의미에서는 표준에 동의하고 표준을 고수하는 한 코딩 표준이 무엇인지는 거의 중요하지 않습니다. . 점점 더 많은 팀과 프로젝트가 PSR을 준수하고 있습니다. Tt는 현재 대다수의 PHP 개발자가 ""표준"으로 인정하고 있으므로 이를 사용하면 새로운 개발자가 팀에 합류할 때 코딩 표준에 익숙하고 편안하게 사용할 수 있습니다.
일반적인 실수 #10: empty()
오용
일부 PHP 개발자는 거의 모든 것에 대한 부울 검사에 empty()
를 사용합니다. 그러나 이것이 혼동을 일으킬 수 있는 경우가 있습니다.
먼저 배열과 ArrayObject
인스턴스(배열을 모방함)로 돌아가 보겠습니다. 유사성을 감안할 때 배열과 ArrayObject
인스턴스가 동일하게 작동한다고 가정하기 쉽습니다. 그러나 이것은 위험한 가정임이 증명됩니다. 예를 들어, PHP 5.0에서:
// PHP 5.0 or later: $array = []; var_dump(empty($array)); // outputs bool(true) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false) // why don't these both produce the same output?
설상가상으로 PHP 5.0 이전에는 결과가 달라졌을 것입니다.
// Prior to PHP 5.0: $array = []; var_dump(empty($array)); // outputs bool(false) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false)
이 접근 방식은 불행히도 꽤 유명합니다. 예를 들어 Zend Framework 2의 Zend\Db\TableGateway
가 doc에서 제안하는 것처럼 TableGateway::select()
결과에서 current()
를 호출할 때 데이터를 반환하는 방식입니다. 개발자는 이러한 데이터로 이러한 실수의 희생자가 되기 쉽습니다.
이러한 문제를 피하기 위해 빈 배열 구조를 확인하는 더 나은 방법은 count()
를 사용하는 것입니다.
// Note that this work in ALL versions of PHP (both pre and post 5.0): $array = []; var_dump(count($array)); // outputs int(0) $array = new ArrayObject(); var_dump(count($array)); // outputs int(0)
그리고 덧붙여 말하면, PHP는 0
을 false
로 캐스트하기 때문에, count()
는 빈 배열을 확인하기 위해 if ()
조건 내에서도 사용될 수 있습니다. PHP에서 count()
는 배열에 대한 지속적인 복잡성( O(1)
연산)이므로 이것이 올바른 선택임을 더욱 분명하게 보여줍니다.
empty()
가 위험할 수 있는 또 다른 예는 이를 매직 클래스 함수 __get()
과 결합하는 경우입니다. 두 개의 클래스를 정의하고 둘 다에 test
속성을 가집시다.
먼저 test
를 일반 속성으로 포함하는 Regular
클래스를 정의해 보겠습니다.
class Regular { public $test = 'value'; }
그런 다음 magic __get()
연산자를 사용하여 test
속성에 액세스하는 Magic
클래스를 정의해 보겠습니다.
class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } }
자, 이제 이러한 각 클래스의 test
속성에 액세스하려고 할 때 어떤 일이 발생하는지 봅시다.
$regular = new Regular(); var_dump($regular->test); // outputs string(4) "value" $magic = new Magic(); var_dump($magic->test); // outputs string(4) "value"
지금까지는 괜찮습니다.
그러나 이제 각각에 대해 empty()
를 호출하면 어떤 일이 발생하는지 봅시다.
var_dump(empty($regular->test)); // outputs bool(false) var_dump(empty($magic->test)); // outputs bool(true)
어. 따라서 empty()
에 의존하는 경우 $magic
의 test
속성이 비어 있지만 실제로는 'value'
로 설정되어 있다고 믿게 될 수 있습니다.
불행히도 클래스가 마법의 __get()
함수를 사용하여 속성 값을 검색하는 경우 해당 속성 값이 비어 있는지 여부를 확인할 수 있는 확실한 방법은 없습니다. 클래스 범위 밖에서는 실제로 null
값이 반환되는지 여부만 확인할 수 있으며 실제로 null
로 설정 되었을 수 있기 때문에 해당 키가 설정되지 않았다는 의미는 아닙니다.
대조적으로, Regular
클래스 인스턴스의 존재하지 않는 속성을 참조하려고 하면 다음과 유사한 알림이 표시됩니다.
Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0
따라서 여기서 요점은 empty()
메서드는 주의하지 않으면 혼란스럽거나 잠재적으로 오해의 소지가 있는 결과를 초래할 수 있으므로 주의해서 사용해야 한다는 것입니다.
마무리
PHP의 사용 용이성은 개발자를 잘못된 편안함으로 유인하여 언어의 일부 뉘앙스와 특이성으로 인해 긴 PHP 디버깅에 취약하게 만들 수 있습니다. 이로 인해 PHP가 작동하지 않고 여기에 설명된 것과 같은 문제가 발생할 수 있습니다.
PHP 언어는 20년의 역사 동안 크게 발전했습니다. 그 미묘함에 익숙해지는 것은 가치 있는 노력입니다. 당신이 생산하는 소프트웨어가 더 확장 가능하고 강력하며 유지 관리가 용이하도록 하는 데 도움이 되기 때문입니다.