5 питань з PHP-тесту Академії, що виявилися найскладнішими для студентів

Щороку команда Binary Studio Academy готується до нового навчального сезону так само старанно, як і апліканти. Ми апдейтимо лекційний матеріал, оновлюємо завдання відбору. Починаємо, зазвичай, із перегляду минулорічних питання зі вступних тестів, аналізу відповідей та вдосконалення першого етапу. В іншій статті Едуард Долинський, JS коуч, докладно розглянув питання JS напрямку. Якщо ви ще не читали його статтю, дуже раджу з нею ознайомитися.

Я також відібрав 5 найбільш проблемних питань з РНР, з якими у 2018 році виникли труднощі у переважної більшості студентів, і спробував їх розібрати.

Для початку пропоную пройти тест по цим питанням:

Пройти тест

Питання Unus

Яким з перелічених методів можна отримати доступ до класів Bar з просторів імен A, A\B, A\C, в просторі імен D в PHP 7.2?

namespace A {
    class Bar {
        public function foo() {
            return 1;
        }
    }
}

namespace A\B {
    class Bar {
        public function foo() {
            return 2;
        }
    }
}

namespace A\C {
    class Bar {
        public function foo() {
            return 3;
        }
    }
}

namespace D {
   // ... here
}

a)

use A\{
    Bar as ABar,
    B\Bar as BBar,
    C\Bar as CBar
};
$a = new ABar;
$b = new BBar;
$c = new CBar;

b)

use A\{B, C};
$a = new A\Bar;
$b = new B\Bar;
$c = new C\Bar;

c)

use A\B;
use A\C;
$a = new A\Bar;
$b = new B\Bar;
$c = new C\Bar;

d)

use A;
use A\B\Bar as BBar;
use A\C\Bar as CBar;
$a = new Bar;
$b = new BBar;
$c = new CBar;

88% відповіли неправильно.

Давайте розберемося, що саме тут відбувається. Є три класи з ім'ям Bar в різних просторах імен A, B і C. Необхідно використати всі ці три класи в просторі імен D, при цьому уникнувши конфлікту імен.

Це питання можна вирішити в один із двох способів: або імпортувати класи з перейменуванням (use A\Bar as ABar), або використовувати ім'я класу разом з простором імені (new A\Bar). Тут потрібно розуміти, як працює оператор use в PHP 7.

У варіантах b) і c) імпортуються простори імен B і C, але інтерпретатор нічого не знає про A, відповідно буде викинуто виняток. У варіанті d) класи з просторів імен B і C імпортуються з перейменуванням, а також імпортується простір імен А. На перший погляд, все зроблено правильно, крім того, що клас Bar, в рядку $a = new Bar; не було імпортовано. Відповідно, відбудеться помилка.

Правильний варіант: a), який використовує можливість PHP 7 імпортувати класи через фігурні дужки:

use A\{
    Bar as ABar,
    B\Bar as BBar,
    C\Bar as CBar
};

Мета цього питання - перевірити знання того, як можна імпортувати класи в PHP 7 і як вирішувати конфлікти імен. Тема про простори імен стала актуальною з введенням автозавантаження класів та є невід'ємною частиною сучасних PHP проектів. Також варто відзначити, що стандарти PSR-4 й PSR-0 (останній вважається застарілим) детально описують правила автозавантаження, які реалізовані і використовуються менеджером залежностей composer.

Детальніше про простори імен можна почитати в офіційній документації PHP.

Питання Duo

Чому буде дорівнювати результат наступного виразу?

[
        function (int $a) {
                return function (int $b) use ($a) {
                        return [ $a * $b ];
                };
        }
][0](2)(2)[0] === ?

a) 4
b) Fatal error
c) 2
d) 0
e) 8

87% відповіли неправильно.

Такий запис виглядає заплутано, і немає жодного приводу використовувати такий код у реальних проектах, але він добре підходить для перевірки знань щодо анонімних функцій або Closure.

По-перше, створюється масив через нотацію квадратних дужок, яка була запроваджена у версії 5.4 та вважається найбільш уживаною . Далі оголошується анонімна функція, яка приймає параметр $a і повертає іншу функцію. Функція, яка повертається, приймає параметр $b і повертає масив з результатом множення $a і $b. Тут варто звернути увагу на оператор use, який використовується для того, щоб передати $a у scope функції, що повертається. Якщо цього не зробити, то $a буде вважатися невизначеною, і буде помилка при спробі помножити $a на $b.

Далі оголошені функції викликаються, і ми отримуємо результат. Не тільки в PHP, але й в інших C-подібних мовах доступ до елементів масиву здійснюється через квадратні дужки, а виклик функцій - через круглі. Відповідно, з масиву обирається функція:

[0](2)(2)[0],

їй в параметр $a передається значення 2:

[0](2)(2)[0],

функції, що повертається, також передається значення 2:

[0]⁠(2)(2)[0],

і потім повертається масив, з якого вибирається добуток $a і $b:

[0]⁠(2)(2)[0].

Відповідно, правильна відповідь a), оскільки 2 * 2 = 4.

Анонімні функції принесли функціональні можливості -у мову PHP й використовуються повсюдно. Наприклад, у функціях array_filter, array_map, array_reduce. Робота з функціями відмінно описана в документації.

Питання Tres

Що не дозволяється в PHP 7.2 вказати у якості типу, що повертається?

a) void
b) function
c) iterable
d) callable
e) int

85% відповіли неправильно.

Щоб відповісти на це питання, потрібно знати про можливості PHP 7.0 визначати у функції типи значень, що повертаються, знати типи даних PHP, а також про те, що з версії PHP 7.1 можна явно вказати, що функція не повертає ніяких значень. Ґрунтуючи відповідь на цих даних, можна зробити висновок, що правильний варіант b) function, тому що такого типу в PHP немає. А якщо необхідно вказати, що повертається функція, то вказується callable або \Closure.

Питання Quattuor

Виберіть спосіб виклику методу foo() з методу bar(), щоб, викликаючи метод bar() на нащадку класу А (наприклад, клас B), викликався метод foo() відповідного класу.

class A {
    static function foo() {
        // …
    }
    static function bar() {
        // ... foo() ?
    }
}
class B extends A {
    static function foo() {
        // ...    
    }
}

a) static::foo()
b) self::foo()
c) $this->foo()
d) (__CLASS__)::foo()
e) base::foo()

80% відповіли неправильно.

Завдання полягає в тому, щоб при виклику методу B::bar() виконувалася логіка методу B::foo(), а не A::foo(), в якому метод bar() визначено.

Варіанти a) self::foo() або d) (__CLASS __)::foo() однакові. Буде використаний метод foo() класу A, а не B, як очікується в завданні. Так відбувається, тому що ключове слово self вказує на той клас, в якому воно використовується. Тобто при спадкуванні динамічне заміщення методу працювати не буде. Це стосується і константи __CLASS__, яка містить ім'я того класу, в якому вона використовується.

Якщо взяти варіант c) $this->foo(), то буде викинуто виняток, бо ключове слово $this належить тільки до екземпляру класу (або об'єкту), а bar() - статичний метод.

Ключового слова base в PHP немає, отже, й e) - також неправильна відповідь.

Для вирішення цього завдання в PHP реалізовано пізнє статичне зв'язування (LSB - Late Static Binding), і для цього використовується ключове слово static. Відповідно, правильна відповідь a) static::foo().

LSB часто використовується, коли логіка статичного батьківського методу змінюється в залежності від дочірнього класу, на якому він викликається. При використанні late static binding не можна забувати про принципи підстановки Лісков та відкритості/закритості з SOLID.

Питання Quinque

Є два трейти, в яких збігаються деякі методи. Як в PHP 7.2 можна визначити обидва трейти в класі, якщо в цьому класі також є методи з іменами, які співпадають таким чином, щоб усі публічні методи трейтів і класу були доступні на інстансі цього класу?

trait Alpha {
    function foo() {}
    function bar() {}
}
trait Beta {
    function foo() {}
    function rar() {}
}
class Gamma {
    use Alpha, Beta;
    function bar() {}
}

a)

use Alpha, Beta {
	Alpha::foo insteadof Beta;
	Beta::foo insteadof Alpha;
}

b)

use Alpha, Beta {
	Alpha::foo insteadof Beta;
	Beta::foo as bFoo;
}

c)

use Alpha, Beta {
	Alpha::foo insteadof Beta;
	Beta::foo as bFoo;
	Alpha::bar as aBar;
}

d)

use Alpha { bar as aBar; }
use Beta { foo as bFoo; }

74% відповіли неправильно. Трейт - це механізм перевикористання коду класами. Трейти необхідні для того, щоб не дублювати код або не створювати нескінченну ієрархію класів. У класі може бути використано необмежену кількість трейтів, відповідно, виникає питання вирішення конфліктів імен. Чи це не привід додати таке питання в тест?

Давайте розглянемо питання детально. У трейті Alpha є методи foo() і bar(), в трейті Beta є метод foo(), як і в Alpha. І в класі Omega є метод bar(), як і в Alpha. Ми хочемо, щоб усі методи (Alpha::foo(), Alpha::bar(), Beta::foo(), Beta::rar(), Omega::bar()) були доступні в класі Omega.

У варіанті а) ми використовуємо метод Alpha::foo() замість Beta::foo() і в той самий час Beta::foo() замість Alpha::foo(). І, як наслідок, буде викинуто виняток при спробі звернутися до методу foo() через те, що не зрозуміло, який з методів foo() необхідно використовувати.

У варіанті b) метод foo() буде доступний із трейту Alpha, а до методу трейта Beta можна буде звернутися по імені bFoo(). Але метод bar() все ще доступний тільки з класу Omega. Тож це також неправильна відповідь.

У варіанті d) буде Fatal Error, тому що відбудеться колізія імен методів Alpha::foo() й Beta::foo(), тому що явно не було вказано, який з методів повинен бути використаний замість якого.

У варіанті c) метод Alpha::foo() перекриває Beta::foo(), метод Beta::foo() доступний по імені bFoo(), Alpha::bar() - по імені aBar(), Omega::bar() і Beta::rar() доступні за своїми іменами. Отже, варіант c) - правильний.

Не тільки трейти, але й всю тему об'єктів та класів з документації важливо знати, щоб успішно пройти перший відбірковий етап.

Підсумок

Решта питань, що стосуються PHP, мають аналогічний вигляд. Маючи базові знання PHP, а також виділивши необхідний час на підготовку й повторення, ви легко впораєтеся з тестом.

Хотілося б відзначити, що при підготовці до тесту варто звернути увагу на сучасні можливості синтаксису мови, концепції об'єктно-орієнтованого програмування, знання суперглобальних масивів, роботу зі структурами даних (масиви, списки) і рядками, а також на методи роботи з HTTP.

Краще джерело для підготовки - офіційний сайт php.net. Також в особистому кабінеті зібрано список літератури, яким не варто нехтувати. Крім того, бажано знати стандарти написання коду PSR-1 та PSR-2, хоч ці знання і не перевірятимуться на тесті.

Пишіть чистий код та хай щастить з тестом.