Подключаем SOAP веб-сервисы в Yii2
Многие сайты и некоторые серверные приложения позволяют обращаться к ним по сети посредством стандартизированного протокола SOAP. Они выкладывают открытый API, через который позволяют вызывать некоторые их методы с передачей параметров. При этом масштабы таких систем могут быть совершенно разными: получение прогноза погоды, сеть 1C крупной организации или система бронирования авиабилетов.
В веб-разработке часто приходится интегрировать сторонние сервисы в свой сайт. Например, предположим, что нам нужно получить время восхода солнца, время захода и долготу дня для определённого города с веб-сервиса GisMeteo.
Мы на адрес http://ws.gismeteo.ru/Weather/Weather.asmx
посылаем POST-запрос на вызов метода GetSunInfo
в виде XML:
POST /Weather/Weather.asmx HTTP/1.1 Host: ws.gismeteo.ru Content-Type: text/xml; charset=utf-8 SOAPAction: "http://ws.gismeteo.ru/GetSunInfo" Content-Length: ... <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <GetSunInfo xmlns="http://ws.gismeteo.ru/"> <serial>...</serial> <townID>...</townID> <date>...</date> </GetSunInfo> </soap:Body> </soap:Envelope>
И с сервера возвращается нужный нам ответ GetSunInfoResponse
в XML-формате:
HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: ... <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <GetSunInfoResponse xmlns="http://ws.gismeteo.ru/"> <GetSunInfoResult> <dt>...</dt> <riseMinutesOffset>...</riseMinutesOffset> <setMinutesOffset>...</setMinutesOffset> <durationMinutes>...</durationMinutes> <result> <errorCode>OK</errorCode> <errorMessage/> </result> </GetSunInfoResult> </GetSunInfoResponse> </soap:Body> </soap:Envelope>
Аналогично представим, что некий магазин открыл SOAP-сервис базы 1С для своих филиалов по адресу http://api.site.ru/ws/webservice.1cws
, закрыв его от посторонних глаз HTTP-Basic авторизацией. Теперь работники других филиалов могут подключить свои экземпляры программы к этому общему сервису и просматривать списки заказов или отгрузок из общей базы и создавать свои:
На стороне сервера нужно только запрограммировать нужные методы вроде GetOrders
и опубликовать сервис.
Это даёт возможность сделать корпоративный веб-портал для работников организации, подключенный к сервису 1С. Или даже разработать мобильное приложение.
Действительно, по этому адресу мы можем послать POST-запрос GetOrders
:
POST /ws/webservice.1cws HTTP/1.1 Host: api.site.ru Content-Type: text/xml; charset=utf-8 SOAPAction: "http://api.site.ru/#WebService:GetLastOrders" Content-Length: ... Authorization: Basic U2l0ZUlVU1I8YUpTM2xxM20= <?xml version="1.0" encoding="UTF-8" ?> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://api.site.ru/"> <SOAP-ENV:Body> <ns1:GetOrders> <ns1:Date1>2014-07-21 23:59:00</ns1:Date1> <ns1:Date2>2014-04-22 00:00:00</ns1:Date2> </ns1:GetLastOrders> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
и получить список заказов, возвращаемый в виде вложенного XML-файла в поле return
:
<?xml version="1.0" encoding="UTF-8" ?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Header/> <soap:Body> <m:GetOrdersResponse xmlns:m="http://api.site.ru/"> <m:return xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <?xml version="1.0"?> <Root> <Документ> <НомерЗаказа>123</НомерЗаказа> <ДатаЗаказа>24.12.2013 15:02:17</ДатаЗаказа> ... <Статус>Выполнен</Статус> </Документ> <Документ> <НомерЗаказа>124</НомерЗаказа> <ДатаЗаказа>24.12.2013 16:30:42</ДатаЗаказа> ... <Статус>Новый</Статус> </Документ> <Root> </m:return> </m:GetLastOrdersResponse> </soap:Body> </soap:Envelope>
Мы просто отправляем запрос на адрес http://api.site.ru/ws/webservice.1cws
для вызова метода с полным именем http://api.site.ru/#WebService:GetOrders
и оформляем XML запрос с использованием соответствующего пространства имён http://api.site.ru/
.
Эти три параметра должны быть именно такими, какими их предоставляет сервер.
Смотря на такие «адские» XML-структуры вам наверняка покажется, что это ужасно сложно использовать. На самом деле, это только эмоциональное первое впечатление. Как бы это ни выглядело сложным, вам не придётся разбираться в формате и что-то парсить.
В PHP для работы с веб-сервисами имеется класс SoapClient. Если вы работаете в Windows и столкнулись с отсутствием этого класса, то подключите расширение php_soap.dll
в файле php.ini
.
Работать с этим компонентом можно в двух режимах. Различаются они только способом формирования запросов.
Работа в WSDL-режиме
Обычно на сервере имеется специально сгенерированный WSDL-файл c описанием всех доступных методов и адресов веб-сервиса. У GisMeteo он выглядит так.
Для уже знакомого нам метода GetSunInfo
в нём имеется отдельная секция с определением полей запроса и ответа:
<s:element name="GetSunInfo"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="serial" type="s:string"/> <s:element minOccurs="1" maxOccurs="1" name="townID" type="s:int"/> <s:element minOccurs="1" maxOccurs="1" name="date" type="s:dateTime"/> </s:sequence> </s:complexType> </s:element> <s:element name="GetSunInfoResponse"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="GetSunInfoResult" type="tns:GetSunInfoResult"/> </s:sequence> </s:complexType> </s:element> <s:complexType name="GetSunInfoResult"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="dt" type="s:dateTime"/> <s:element minOccurs="1" maxOccurs="1" name="riseMinutesOffset" type="s:int"/> <s:element minOccurs="1" maxOccurs="1" name="setMinutesOffset" type="s:int"/> <s:element minOccurs="1" maxOccurs="1" name="durationMinutes" type="s:int"/> <s:element minOccurs="1" maxOccurs="1" name="result" type="tns:ServiceResult"/> </s:sequence> </s:complexType>
Ниже находится информация о необходимом для его вызова значении action
и используемых режимах работы:
<wsdl:operation name="GetSunInfo"> <soap:operation soapAction="http://ws.gismeteo.ru/GetSunInfo" style="document"/> <wsdl:input> <soap:body use="literal"/> </wsdl:input> <wsdl:output> <soap:body use="literal"/> </wsdl:output> </wsdl:operation>
И в самом конце файла расположена секция для определения location
для работы по протоколам SOAP 1.1 и SOAP 1.2 соответственно:
<wsdl:service name="Weather"> <wsdl:port name="WeatherSoap" binding="tns:WeatherSoap"> <soap:address location="http://ws.gismeteo.ru/Weather/Weather.asmx"/> </wsdl:port> <wsdl:port name="WeatherSoap12" binding="tns:WeatherSoap12"> <soap12:address location="http://ws.gismeteo.ru/Weather/Weather.asmx"/> </wsdl:port> </wsdl:service>
Аналогичные секции присутствуют в WSDL файле, генерируемом веб-сервисом 1С.
Формат запроса заказов может быть таким:
<xs:element name="GetOrders"> <xs:complexType> <xs:sequence> <xs:element name="Date1" type="xs:dateTime" nillable="true"/> <xs:element name="Date2" type="xs:dateTime" nillable="true"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="GetOrdersResponse"> <xs:complexType> <xs:sequence> <xs:element name="return" type="xs:string"/> </xs:sequence> </xs:complexType> </xs:element>
Полное имя метода с пространством имён таким:
<operation name="GetOrders"> <soapbind:operation style="document" soapAction="http://api.site.ru/#WebService:GetOrders"/> <input> <soapbind:body use="literal"/> </input> <output> <soapbind:body use="literal"/> </output> </operation>
А адреса доступа такими:
<service name="WebService"> <port name="WebServiceSoap" binding="tns:WebServiceSoapBinding"> <soapbind:address location="http://api.site.ru/ws/webservice.1cws"/> </port> <port name="WebServiceSoap12" binding="tns:WSVolhovecSoap12Binding"> <soap12bind:address location="http://api.site.ru/ws/webservice.1cws"/> </port> </service>
Теперь достаточно указать этот файл при создании объекта класса SoapClient
:
$client = new SoapClient('http://ws.gismeteo.ru/Weather/Weather.asmx?WSDL');
или воспользоваться локальной копией файла:
$client = new SoapClient(__DIR__ . '/webservice.wsdl');
Так как сервис может быть закрыт HTTP Basic авторизацией, мы можем указать данные для доступа:
$client = new SoapClient(__DIR__ . '/webservice.wsdl', [ 'login' => 'soap_username', 'password' => 'soap_password', ]);
В классе SoapClient
реализован магический метод __call
, который позволяет работать с удалёнными методами прямо по их имени:
$result = $client->GetSunInfo([ 'serial' => '...', 'townID' => 57, 'date' => '2014-01-31', );
Результат $result
не надо парсить вручную, так как он уже будет представлять из себя объект с полями, повторяющими XML-структуру ответа. Например, долготу дня можно будет получить так:
echo $result->GetSunInfoResult->durationMinutes;
Аргументы метода могут передаваться в виде массива или объекта. Попробуем, например, получить курсы валют на текущий день по протоколу SOAP с сайта Центробанка с помощью метода GetCursOnDate
. В отличие от получения погоды, этот метод возвращает ответ с единственным полезным нам полем any
, в котором содержится вложенный XML документ с валютами. Но ничего страшного, так как его легко будет превратить в объект с помощью SimpleXMLElement
.
Создадим файл curs.php
:
$wsdl = 'http://www.cbr.ru/DailyInfoWebServ/DailyInfo.asmx?WSDL'; $client = new SoapClient($wsdl, [ 'exceptions' => 1, 'cache_wsdl' => WSDL_CACHE_MEMORY, ]); $result = $client->GetCursOnDate([ 'On_date' => date('Y-m-d'), ]); $data = new SimpleXMLElement($result->GetCursOnDateResult->any); foreach ($data->ValuteData->ValuteCursOnDate as $curs) { printf('%s = %s Руб', trim($curs->Vname), floatval($curs->Vcurs) / floatval($curs->Vnom)); echo PHP_EOL; }
и запустим его в консоли:
php curs.php
Через некоторое время мы увидим результат:
Австралийский доллар = 29.1544 Руб Азербайджанский манат = 41.6884 Руб ... Вон Республики Корея = 0.0309349 Руб Японская иена = 0.311228 Руб
Здесь мы WSDL-файл загружаем прямо с сервера и кешируем его в памяти на некоторое время. Но если в каком-либо сервисе этот файл занимает несколько мегабайт, то для экономии трафика его лучше скачать к себе и подключать из локальной папки.
Вот и всё. Создаём клиент и выполняем методы. Никаких мучений с составлением и парсингом SOAP-запросов и ответов.
Работа в ручном режиме
Если у вас есть необходимость подключаться к какому-то специфическому веб-сервису, у которого отсутствует WSDL-файл, либо если вы хотите указывать все параметры вручную, то вам будет нужен режим работы без WSDL.
В данном случае клиенту неоткуда будет брать информацию об адресе сервиса, пространствах имён и полных имён методов для SOAPAction
. Поэтому адрес сервиса location
и пространство имён uri
надо будет указать вручную:
$location = 'http://api.site.ru/ws/webservice.1cws'; $uri = 'http://api.site.ru/' $username = 'username'; $password = 'password'; $client = new SoapClient(null, [ 'use' => SOAP_LITERAL, 'style' => SOAP_DOCUMENT, 'location' => $location, 'uri' => $uri, 'login' => $username, 'password' => $password, 'exceptions' => 1, 'soap_version' => SOAP_1_1, ]);
Параметром exceptions
мы включаем механизм генерирования исключений, позволяющий обрабатывать их конструкцией try { ... } catch (SoapFault $e)
. Другими параметрами можно указать, например, используемый прокси-сервер.
Все методы класса SoapClient
доступны извне. Послать команду на сервер теперь нужно непосредственным вызовом __soapCall
, указав имя метода для построения XML-запроса и полное имя SOAPAction. Получим из 1С список заказов за последние 90 дней:
$date_from = (new \DateTime())->sub(new \DateInterval('P90D'))->format('Y-m-d 00:00:00'); $date_to = (new \DateTime())->format('Y-m-d 23:59:59'); $method = 'GetOrders'; $action = 'http://api.site.ru/#WebService:GetOrders'; $params = [ new SoapParam($date_from, 'Date1'); new SoapParam($date_to, 'Date2'); ]; $options = [ 'soapaction' => $action, ]; $result = $client->__soapCall($method, $params, $options);
Метод __soapCall
позаботится о построении XML-структуры запроса и вызовет метод __doRequest
, который отправит запрос на сервер.
Но при желании мы можем опуститься ещё ниже и вызвать метод __doRequest
. В этом случае XML-запрос нужно составить самому:
$action = '#http://api.site.ru/#WebService:GetOrders'; $request_xml = '<' . '?xml version="1.0" encoding="UTF-8"?' . '> <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="' . $uri . '"> <SOAP-ENV:Body> <ns1:GetLastOrders> <ns1:Date1>' . $date_from . '</ns1:Date1> <ns1:Date2>' . $date_to . '</ns1:Date2> </ns1:GetLastOrders> </SOAP-ENV:Body> </SOAP-ENV:Envelope>'; $response = $client->__doRequest($request_xml, $location, $action, SOAP_1_1);
Смешанное использование
Иногда встречаются ситуации, когда нужно использовать нестандартные параметры запроса. Например, специфические заголовки. Рассмотрим пример получения информации о пользователе из eBay:
$token = $_SESSION['ebay_token']; $appId = $config['ebay']['appId']; $wsdl = 'http://developer.ebay.com/webservices/latest/eBaySvc.wsdl'; $method = 'GetUser'; $client = new SOAPClient($wsdl, [ 'exceptions' => 1, 'location' => 'https://api.sandbox.ebay.com/wsapi?callname=' . $method . '&appid=' . $appId . '&siteid=0&version=821&routing=new', ]); $auth = new stdClass(); $auth->eBayAuthToken = $token; $header = new SoapHeader('urn:ebay:apis:eBLBaseComponents', 'RequesterCredentials', $auth); $params = [ 'Version' => 821, 'DetailLevel' => 'ReturnSummary', 'UserID' => '', ]; $response = $client->__soapCall($method, $params, null, $header);
Можно заметить, что здесь есть WSDL файл, но для работы нам нужно передавать токен авторизованного пользователя в дополнительных заголовках. И в этом случае нам помог вызов метода __soapCall
.
Интеграция с веб-сервисом в Yii2
Аналогично нашим предыдущим скриптам, мы можем производить операции в любом месте нашего приложения:
use SoapClient; ... $wsdl = Yii::getAlias('@app/config/webservice.wsdl'); $client = new SoapClient($wsdl, [ 'login' => Yii::$app->params['soap_username'], 'password' => Yii::$app->params['soap_password'], 'exceptions' => 1, 'soap_version' => SOAP_1_2, 'cache_wsdl' => WSDL_CACHE_MEMORY, ]);
То есть для данного случая нам просто нужно поместить WSDL-файл в любое место и прописать авторизационные данные в конфигурационном файле.
Это простейший случай, но использование этого кода, например, в контроллере не очень уместно. Вместо этого клиент целесообразнее вынести в отдельный компонент. Этим мы сейчас и займёмся.
Вместо использования динамически создаваемых структур или ассоциативных массивов для передачи аргументов запроса:
$request = new stdClass(); $request->serial = '...'; $request->townID = 57; $request->date = date('Y-m-d');
лучше использовать одноимённые запросам классы:
class GetSunInfo { public $serial; public $townID; public $date; } $request = new GetSunInfo(); $request->serial = '...'; $request->townID = 57; $request->date = date('Y-m-d'); $response = $client->GetSunInfo($request);
Это удобнее тем, что в любом хорошем редакторе или IDE работает автоподстановка полей и подсветка опечаток. Кроме того, в своём классе мы можем релизовать любой дополнительный функционал.
Например, создав базовый класс для наших команд, наследуемый от класса yii\base\Model
:
namespace app\components\webservice\request; use yii\base\Model; abstract class BaseRequest extends Model {}
мы превращаем все структуры в модели и легко можем добавить в них правила валидации:
namespace app\components\webservice\request; class GetSunInfo extends BaseRequest { public $serial; public $townID; public $date; public function rules() { return [ [['serial', 'townID', 'date'], 'required'], ['date', 'date'], ] } }
и проверять корректность заполненных данных перед отправкой запроса:
$request = new GetSunInfo(); $request->serial = '...'; $request->townID = 57; $request->date = date('Y-m-d'); if ($request->validate()) { $response = $client->GetSunInfo($request); } else { print_r($request->getErrors()); }
И ещё один нюанс. У нас сделано так, что имя метода и класса его параметров совпадают:
$request = new GetSunInfo(); $response = $client->GetSunInfo($request);
Соответственно, имя метода можно получить из имени класса:
$method = pathinfo(str_replace('\\', '/', get_class($request)), PATHINFO_BASENAME);
и вызывать этот метод у клиента динамически любым способом из этих двух:
$response = $client->{$method}($request); $response = call_user_func_array([$this->client, $method], [$request]);
Теперь, пользуясь этими нюансами, мы можем создать компонент приложения с единственным методом send
, который будет автоматически определять имя метода и вызывать его у клиента:
namespace app\components\webservice; use app\components\webservice\request\BaseRequest; use yii\base\Component; use SoapClient; use Yii; class WebService extends Component { public $wsdl = ''; public $username = ''; public $password = ''; /** * @var SoapClient */ private $client; public function init() { $this->createSoapClient(); parent::init(); } public function send(BaseRequest $request) { $method = pathinfo(str_replace('\\', '/', get_class($request)), PATHINFO_BASENAME); return @call_user_func_array([$this->client, $method], [$request]); } protected function createSoapClient() { $wsdl = Yii::getAlias($this->wsdl); $this->client = new SoapClient($wsdl, [ 'trace' => 1, 'compression' => SOAP_COMPRESSION_ACCEPT, 'login' => $this->username, 'password' => $this->password, 'exceptions' => 1, 'soap_version' => SOAP_1_2, 'cache_wsdl' => WSDL_CACHE_MEMORY, ]); } }
и подключить его в конфигурационном файле:
return [ 'components' => [ ... 'webservice' => [ 'class' => 'app\components\webservice\WebService', 'wsdl' => '@app/config/webservice.wsdl', 'username' => 'user', 'password' => 'password', ], ], ];
чтобы теперь обращаться к нему как к Yii::$app->webservice
:
$request = new GetSunInfo(); $request->serial = '...'; $request->townID = 57; $request->date = date('Y-m-d'); if ($request->validate()) { $response = Yii::$app->webservice->send($request); ... } else { ... }
Этого уже вполне достаточно для нормальной работы.
Но если у вас очень сложный сервис, ответы которого нужно довольно рутинно парсить (или ответ приходит в нестандартной кодировке, которую надо всегда расшифровывать), то можно наделить логикой и ответы.
Создадим базовый класс для ответа:
namespace app\components\webservice\response; abstract class BaseResponse { public $result = ''; public function __construct($result) { $this->result = $result; } }
Теперь отнаследуем от него класс, который будет обрабатывать ответ метода получения курса валют GetCursOnDate
. Одноимённый класс, но в папке response
:
namespace app\components\webservice\response; class GetCursOnDate extends BaseResponse { private $_data; public function getCursByCode($code) { foreach ($this->getData()->ValuteCursOnDate as $curs) { if ($curs->VchCode == $code) { return floatval($curs->Vcurs) / floatval($curs->Vnom); } } return null; } private function getData() { if ($this->_data === null) { $xml = $this->result->ValuteData->GetCursOnDateResult->any; $this->_data = new SimpleXMLElement($xml); } return $this->_data; } }
Дополним наш компонент функционалом оборачивания пришедшего от клиента ответа в полученный из имени метода класс:
use app\components\webservice\request\BaseRequest; use app\components\webservice\response\BaseResponse; class WebService extends Component { ... /** * @param BaseRequest $request * @return BaseResponse */ public function send(BaseRequest $request) { $method = pathinfo(str_replace('\\', '/', get_class($request)), PATHINFO_BASENAME); $response = @call_user_func_array([$this->client, $method], [$request]); $class = '\app\components\webservice\response\\' . $method; return new $class($response); } }
После этого вместо голого ответа от клиента в переменной $response
мы уже получим наш объект и сможем использовать его методы.
Теперь весь код получения курса доллара через SOAP клиент занимает всего несколько строк:
use app\components\webservice\request\GetCursOnDate; use app\components\webservice\response\GetCursOnDate as GetCursOnDateResponse; $request = new GetCursOnDate(); $request->On_date = date('Y-m-d'); /** @var GetCursOnDateResponse $response */ $response = Yii::$app->webservice->send($request); echo $response->getCursByCode('USD');
и может полноценно использоваться в любом месте приложения. Всё связанное с SOAP мы полностью инкапсулируем внутри нашего компонента webservice
. Теперь в тестовой конфигурации можно легко подменить класс WebService
, чтобы заменить рабочий компонент на его заглушку.
После того, как оставите комментарий к данной статье, советую прочесть ещё пару интересных статей:
До встречи!
Интересно, но 30 писем этого выпуска рассылки подписчикам не дошли и были отклонены как спам... Подозреваю, что это ящики на mail.ru.
У меня ящик на mail.ru, письмо пришло. Спасибо за статью!
Здравствуйте, Дмитрий. Этот как и др. ваши статьи очень полезны, спасибо. Черканите пожалуйста подробную стать о том как сделать 1С интеграцию с yii или yii2
С самим кодом на 1С я не очень помогу. Механизм как в представленном видео. А со стороны Yii2 всё как в этой статье.
Опечатка:
Должно быть:
Спасибо! Исправил.
Вот тут еще для красоты добавить $ в начале:
Спасибо за интересные статьи.
опечатка в кавычках:
Спасибо! Исправил.
Здравствуйте, Дмитрий!
К сожалению не доступно видео из этой статьи. Или это просто скринкаст данной статьи?
Заранее благодарю!
Проверил. Доступно.
Спасибо больше за статью! Очень помогла
У вас тут опечатка или специально зачем-то подчеркивание перед data?
ps: "Ваш сайт" не дает заполнить https. :)
Опечатку исправил. Подчёркивание модно в Yii2.
Здравствуйте!
Не подскажете как лучше подключить СМС рассылку xml запросом.
Рассылать должно как одному, так и выбраной группе.
Адрес сервера
Описание команд
sendmessage
Отправка пакета СМС сообщений.
Запрос
Ответ
querymessage
Запрос статуса пакета СМС сообщений.
Запрос
Ответ
Это не SOAP, а простое XML-API. Формируем XML через DomDocument и посылаем POST-запросом вручную через CURL либо через любой клиент вроде Guzzle или Yii2 HTTP Client. И ответ парсим через simplexml_load_string в объект.
спасибо, попробую
Здравствуйте, Дмитрий.
Не подскажете как сделать такое:
Есть soap метод добавления AddBrand(BrandContract toAdd)
в который передается структура BrandContract вида BrandContract.Name BrandContract.Description
Как передать в метод структуру данных средствами php?
Дмитрий,
А как быть если параметры метода веб-сервиса выглядит примерно так: "dateFrom-TIMESTAMP_x0020_WITH_x0020_TIME_x0020_ZONE-IN"?
PHP не дает создать такое свойство.
Спасибо, помогло
точку с запятой после $uri = ... пропустили
Хорошая статья, все понятно стало, только делаю под симфони.
Хотелось бы больше статей по symfony(
В Symfony используются вещи и паттерны из нативного PHP без привязки к фреймворку. И там хорошая документация. Так что в статьях не особо понятно, о чём ещё можно там рассказть.