Ответы пользователя по тегу PHP
  • Как исправить ошибку Trying to access array offset on value of type null при проверке не существующих логина и пароля?

    nowm
    @nowm
    fetch_assoc вместо массива возвращает null, если запрос вернул пустой результат. У вас переменная $result равна null. Об этом прямо написано в ошибке.

    if (
        is_array($result)
        && $telegram_id === $result["telegram_id"]
        && $password === $result["password"]
    ) {
        echo "пароль верный";
    }


    Кстати, советую эту статью почитать: https://habr.com/ru/post/148701/. К слову, рано или поздно вы с автором этой статьи тут столкнётесь (если он не забанен сейчас), потому что он мимо такого кода не проходит, и у него большая практика тыкания лицом в какашки.
    Ответ написан
    Комментировать
  • Как остановить ratchet из php?

    nowm
    @nowm
    Можно это сделать двумя способами:

    1. Доработать PHP-код, чтобы он реагировал на сигналы, переданные через вебсокет. Например, если ему послать команду «stop», то он должен останавливаться и, соответственно, процесс завершается, освобождая TTY или тот скрипт, который запускал этот вебсокет.
    2. Скрипт с вебсокетом можно запускать в фоновом режиме, а потом, если нужно его завершить, просто убивать процесс по его PID. Чтобы запустить скрипт в фоновом режиме, нужно добавить & в конце команды. Для примера: php bin/chat-server.php &


    Судя по вашей терминологии («перезагрузка php») вы из скрипта PHP вызываете команду php bin/chat-server.php, а сам этот скрипт вызывается с помощью HTTP-запроса из браузера. В этом случае имеется ввиду перезагрузка PHP-FPM, так? Если так, то самое простое решение проблемы с подвисанием скрипта — это способ 2.
    Ответ написан
    6 комментариев
  • Возможно ли с помощью Node.js/php получить скрытый контент определенной странице определенного сайта?

    nowm
    @nowm
    Я обычно пользуюсь DOM в PHP. Конечно, можно отдельные поля регулярными выражениями отловить, если знаешь их название, но есть большая вероятность, что у одного инпута сначала будет идти `type`, потом `name`, а у другого наоборт, и придётся для каждого инпута писать отдельный код, который будет из него вытаскивать значение. Проще воспринимать их как сущности с одинаковыми признаками и переложить парсинг на специализированные билиотеки.

    Насколько я понял, судя по особенностям наименования инпутов, это был форум на базе hoop.la (не путать с hoopla). Я бы примерно так организовал парсинг:

    $dom = new \DOMDocument();
    
    libxml_use_internal_errors(true);
    if (!@$dom->loadHTML('содержимое страницы в виде HTML')) {
        /** @var \LibXMLError $error */
        $error = libxml_get_last_error();
        if ($error->level > LIBXML_ERR_ERROR) {
            throw new \Exception($error->message);
        }
    }
    
    $xpath = new \DOMXPath($dom);
    
    /** @var \DOMNodeList $form */
    $form = $xpath->query('//form[@name="mainLoginForm"]');
    if (!$form->length) {
        throw new \Exception('Форма не найдена');
    }
    
    $post_data = [];
    
    /** @var \DOMElement $input */
    foreach ($xpath->query('.//input', $form->item(0)) as $input) {
        $post_data[$input->getAttribute('name')] = $input->getAttribute('value');
    }
    
    $post_data['email'] = 'логин';
    $post_data['password'] = 'пароль';
    
    // В $post_data находятся все данные которые нужно отправлять


    Суть такого подхода в том, что особенно не нужно загоняться, какие там есть поля и как они называются. Обходим все доступные инпуты в конкретной форме и засовываем их в массив, потом поверху патчим этот массив своим логином и паролем — всё, все необходимые данные, включая различные токены, у нас есть.

    В каких-то других ситуациях, кроме инпутов нужно будет искать ещё и селекты, или предусматривать ситуации, когда нужно делать выбор из нескольких radio-элементов, либо предусматривать ещё более комплексные ситуации, но конкретно в случае с hoopla этого не потребуется — там одни инпуты, и они динамически с помощью JS вроде бы не модифицируются (сильно не тестировал).

    Код может быть нерабочим. Я его наполовину копипастил из собственных разработок, наполовину писал прямо на Тостере. Главное, из него понятен смысл.
    Ответ написан
    5 комментариев
  • Как правильно сравнить массивы и оценить их схожесть?

    nowm
    @nowm
    Если два массива имеют одинаковую длину, можно просто двигать по кругу первый массив и сравнивать его элементы с элементами второго. Потом можно просто выбрать максимальное совпадение и перегнать в проценты. Примерно так:

    $arr1 = [1,2,3,4,5,7,2,8];
    $arr2 = [2,9,5,5,7,2,8,1];
    
    $len = count($arr1);
    $conformity = [];
    
    for($i = 0; $i < $len; $i++) {
    	/**
    	 * $temp содержит нули в позициях, где числа в двух массивах 
    	 * по одному и тому же индексу не равны. Единицы — там, где равны.
    	 */
    	$temp = array_map(function($x,$y){return intval($x==$y);}, $arr1, $arr2);
    	
    	// Элементы полученного массива суммируются и добавляются в отчётный массив
    	$conformity[] = array_sum($temp);
    	
    	// Массив прокручивается на одну позицию
    	$arr1[] = array_shift($arr1);
    }
    
    //С помощью max($conformity) выбирается максимальное совпадение элементов
    echo sprintf("Max conformity is %s%%\n", number_format(100*(max($conformity)/$len), 2));


    Это конкретно для ситуации, когда длина «колец» одинаковая.

    Update: ещё один вариант:

    $arr1 = [1,2,3,4,5,7,2,8];
    $arr2 = [2,9,5,5,7,2,8,1];
    
    function conformity($arr1, $arr2) {
    	$len = count($arr1);
    	$max = $curr = 0;
    	
    	for($i = 0; $i < $len; $i++) {
    		array_map(function($x,$y)use(&$curr){$curr += intval($x==$y);}, $arr1, $arr2);
    		
    		if($curr == $len) {
    			return 100;
    		}
    
    		$max = $max > $curr ? $max : $curr;
    		$curr = 0;
    		
    		$arr1[] = array_shift($arr1);
    	}
    	
    	return 100*($max/$len);
    };
    
    echo sprintf("Max conformity is %s%%\n", number_format(conformity($arr1, $arr2), 2));
    Ответ написан
    6 комментариев
  • Принцип работы буфферизированного вывода в php?

    nowm
    @nowm
    Чтобы ничего не выводилось, нужно вызывать функцию ob_end_clean.

    Я обычно с буфером так работаю:

    ob_start();
    
    // Тут какой-нибудь вывод
    
    // В переменную $output заносится весь буфер, и её можно потом где-то использовать
    $output = ob_get_contents();
    
    // Очистка буфера
    ob_end_clean();


    Хотя, можно ещё проще делать: вместо связки ob_get_contents + ob_end_clean можно использовать ob_get_clean, которая очищает буфер и при этом возвращает его содержимое.

    ob_start();
    
    // Тут какой-нибудь вывод
    
    // В переменную $output заносится весь буфер, и её можно потом где-то использовать.
    // При этом происходит очистка буфера.
    $output = ob_get_clean();
    Ответ написан
    Комментировать
  • Как на 100% удалить cookie на стороне клиента?

    nowm
    @nowm
    Мне кажется, что хранить user_id в куках — не очень хорошая идея. Пользователю в этой ситуации ничего не помешает на своей стороне поставить себе user_id администратора и делать всё, что может делать администратор.

    Лучше всего хранить идентификатор пользователя в сессии ($_SESSION['user_id']), так как пользователь напрямую не сможет этот идентификатор изменить (перехватить сессию будет сложнее, чем заменить user_id в куках). А всякие пользовательские данные, вроде user_name лучше хранить в БД и вытаскивать, когда требуется. В куках, опять же, которые полностью во власти пользователя, это лучше не хранить.

    И потом, когда вам понадобится разлогинить пользователя, просто уничтожаете сессию и всё.

    session_destroy();

    Куку с идентификатором сессии можно даже не уничтожать с помощью setcookie, так как после уничтожения сессии неважно, что там хранится в куке. Когда будете создавать новую сессию, кука просто заменится кукой с идентификатором новой сессии.

    В Firefox можно просматривать куки с помощью расширения Firebug. Все куки для текущей страницы находятся на вкладке «Cookies» панели Firebug, которая открывается нажатием клавиши F12.
    Ответ написан
    4 комментария
  • Как в printf поставить "%"?

    nowm
    @nowm
    %%
    Ответ написан
    Комментировать
  • Безопасен ли такой код?

    nowm
    @nowm
    Да, в принципе, вполне безопасен. Здесь вы не вызываете никакой пользовательский код. $controller у вас вызывается ваш, который вы вручную прописывали внутри массива $routes.
    Ответ написан
    Комментировать
  • Как получить appid(значение из массива) ?

    nowm
    @nowm
    Вы же затёрли предыдущее значение $rez. Поэтому $rez->response->games — это несуществующее свойство.

    Смотрите сами:

    $rez = json_decode($getUrl); // $rez->response->games существует
    $rez = $rez->response->games; // $rez->response->games больше не существует


    Нужно просто другую переменную использовать. Например так:

    $rez = json_decode($getUrl);
    $app_list = $rez->response->games;


    Тогда у вас $rez->response->games будет доступно и вы сможете делать foreach как для $app_list, так и для $rez->response->games.
    Ответ написан
    1 комментарий
  • Правильно ли сформирован запрос на php, mysql?

    nowm
    @nowm
    1. Точка с запятой необязательна, если выполняется только одна инструкция. В вашем случае её можно не писать, так как выполняется только одна инструкция «INSERT». Если инструкций несколько, то каждая инструкция обязательно должна заканчиваться точкой с запятой (кроме последней инструкции, кажется — там не обязательно).

    2. Да, запрос правильный.

    Тот запрос, который у вас был «как вариант», не будет работать, потому что вы пытаетесь нормальный PHP-шный NULL скопировать в строку. Там не появится слово «NULL» в итоге, так как вместо него подставится пустая строка. Итоговый запрос будет выглядеть примерно так:

    INSERT INTO users (user_id, user_name, user_adddate) VALUES ('', 'user', '1404721764')


    У вас здесь сразу две ошибки. Первая — вы передаёте значение NULL, а нужно передавать строку со словом «NULL». Вторая — вы пытаетесь сгененрировать строку так, что у вас слово «NULL» будет в SQL-запросе стоять в кавычках. В таком случае это для SQL-сервера будет считаться не значением «NULL», а строкой. Будет выдана ошибка, потому что у вас user_id — целочисленное, а вы пытаетесь в него писать строку. Правильнее будет так (разобью на строки для читаемости):

    <?php
    $insert_sql = sprintf("
        INSERT INTO users (user_id, user_name, user_adddate) 
        VALUES (%s, '%s', %d)
    ", 'NULL', $user_name, time());
    ?>


    Со sprintf вы движетесь в правильном направлении. После этого вам будет легко понять принцип работы подготовленных запросов PDO, которые я вам советую использовать вместо mysql_*-функций.

    <?php
    /* 
     * Инициализация. Эту строку нужно писать там, 
     * где вы делаете mysql_connect. А сам вызов mysql_connect 
     * и прочего сопутствующего можно удалить.
     */
    $db = new PDO('mysql:dbname=testdb;host=127.0.0.1', 'dbuser', 'dbpass');
    
    // . . .
    
    /* 
     * Вставляемые переменные заменяются знаками вопроса. Никаких кавычек
     * тут ставить не нужно — PDO сам поставит их в нужном месте.
     */
    $sth = $db->prepare('INSERT INTO `users` (user_id, user_name, user_adddate) VALUES (NULL, ?, ?)');
    
    /* 
     * Передача значений в запрос происходит в виде массива;
     * порядок элементов в массиве имеет значение. 
     * $user_name — первый элемент, значит его значение подставится 
     * на месте первого знака вопроса. time() — второй элемент, значит
     * его результат подставится вместо второго «?»
     */
    
    $sth->execute( array($user_name, time()) );
    ?>


    В запросе выше я добавил NULL прямо в строку, чтобы сильно не загоняться, так как для его добавления нужна дополнительная работа. С дополнительной работой это выглядит примерно так:

    <?php 
    // . . . 
    
    $sth = $db->prepare('INSERT INTO `users` (user_id, user_name, user_adddate) VALUES (?, ?, ?)');
    
    /*
     * Важный момент: нельзя писать
     * $sth->bindParam(1, NULL, PDO::PARAM_NULL);
     * Перед тем, как биндить NULL, его нужно присвоить переменной 
     * и биндить уже эту переменную. Иначе будут выскакивать ошибки.
     */
    $null_value = NULL;
    $sth->bindParam(1, $null_value, PDO::PARAM_NULL);
    
    $sth->bindParam(2, $user_name, PDO::PARAM_STR);
    $sth->bindParam(3, time(), PDO::PARAM_INT);
    
    /*
     * Так как параметры уже переданы с помощью bindParam-ов,
     * в вызов execute не нужно ничего добавлять.
     */
    $sth->execute();
    ?>


    По поводу NOW() или CURDATE(). У вас выскакивает ошибка потому, что это не функции PHP. Это функции MySQL и их нужно писать в запросе как есть — они выполняются не в интерпретаторе PHP.

    Как я понял, вы пытались что-то вроде этого делать:

    <?php
    $insert_sql = sprintf("
        INSERT INTO users (user_id, user_name, user_adddate) 
        VALUES (%s, '%s', %d)
    ", 'NULL', $user_name, NOW());
    ?>


    А нужно так:
    <?php
    $insert_sql = sprintf("
        INSERT INTO users (user_id, user_name, user_adddate) 
        VALUES (NULL, '%s', NOW())
    ", $user_name);
    ?>


    Так же, если вы добавляете user_adddate с помощью NOW() или CURDATE(), лучше для этого столбца изменить тип с bigint(20) на datetime.
    Ответ написан
  • Как использовать имя переменной как строку таблицы PHP PDO?

    nowm
    @nowm
    Вот так:

    $rows = $bd->exec("INSERT INTO test (tab$j) VALUES ('$nomer')");

    У вас не срабатывает текущий вариант из-за того, что вы пишете так: '{$j}'. Дело в том, что в одиночных кавычках подстановка значения переменной не происходит. Для подстановки значения нужно использовать двойные кавычки. Я проиллюстирую:

    $variable = 'John';
    
    echo 'Hello, $variable'; // Выведет: Hello, $variable
    echo "Hello, $variable"; // Выведет: Hello, John

    Так же, судя по вашему коду, под tab0 и tab1 подразумеваются названия полей в таблице test, а не названия отдельных таблиц tab0 и tab1.

    И, под конец: несмотря на то, что вы используете PDO, вы зачем-то пихаете значение переменной в строку запроса напрямую, как это делается при использовании mysql_query(); Это ни на грамм не добавляет безопасности такому запросу. Почитайте про подготовленные запросы в PDO. Если не ошибаюсь, вот такая конструкция может сработать:

    $st = $bd->prepare("INSERT INTO test (tab$j) VALUES (?)");
    $st->execute(array($nomer));
    $rows = $st->rowCount();
    Ответ написан
  • Как улучшить навыки построения веб-приложения (php)?

    nowm
    @nowm
    Ссылка на мое убожество

    Первый и очень важный совет: если у вас возникает желание написать: «Ссылка на мое убожество», лучше сразу сотрите весь код и не отвлекайте людей по пустякам. Смотрите: вы называете его убожеством. Значит вы знаете почему он убогий. В этом случае поправьте его до такой степени, чтобы у вас не появлялось желание самому называть его убогим. Поправили — публикуйте. Если же вы его называете убогим только потому, что кто-то ещё называет его убогим, то просите того человека делать обзор, а не всех вподряд.

    Сегодня мне сказали что я имею фундаментальные навыки построения веб-приложений, но они не дотянут до junior уровня.

    Спросите у того, кто вам это сказал. Поинтересуйтесь, что можно исправить и в каком направлении работать. Просто в одной компании у Senior PHP Programmer может быть даже меньше навыков, чем у Junior PHP Programmer в другой компании. Если кто-то говорит, что код — гавно, попросите объяснить, почему он плохой. Если люди не объясняют, просто забейте и программируйте в своё удовольствие.

    Чётких критериев деления на Junior, Middle и Senior нет. Но есть признаки, по которым можно примерно ориентироваться.

    1. Если человек пишет программу так, что вывод нарушает семантику выходного формата, это явный признак юниорства. Для примера, в файле /framework/index.php есть такой код:

    echo '<!-- Код выполнен за '.number_format(microtime(1)-$t,3).' секунды -->';


    Это выведет примерно такой код:
    <html>
    . . .
    </html>
    <!-- Код выполнен за 0.531 секунды -->


    Семантически это неправильно, так как после закрывающего тега HTML больше не должно ничего быть.

    2. Русские и английские комментарии смешиваются. Вообще, лучше писать комментарии на английском, так как код вы выкладываете на ресурс, где большинство посетителей англоговорящие (я про GitHub, а не про Тостер). При этом, без проблем можно писать комментарии на русском, если вам глубоко параллельно на англоязычную аудиторию. Но в этой ситуации нужно писать все комментарии на русском. У вас же там мешанина — автоматически сгенерированные фреймворком англоязычные комментарии и рядом комментарии на русском языке. В вашем случае нужно либо переписать английские комментарии на русский, либо русские комментарии перевести на английский.

    Дополнительно, не нужно писать комментарии для обыденной фигни, вроде:
    $config['per_page'] = 10; // сколько записей показывать на странице

    Это тоже самое, что наклеить на монитор заметку, что Земля вращается вокруг Солнца. Вам лично нужно постоянно самому себе напоминать этот факт? Возможно, есть смысл напоминать какие-то сложные моменты, вроде расстояния между Солнцем и Землёй в миллиметрах, но не обыденные вещи.

    3. Не-единообразный подход к оформлению кода. Например, фреймворк сгенерировал такой код:

    if (defined('ENVIRONMENT'))
    {
        switch (ENVIRONMENT)
        {
            case 'development':
                error_reporting(E_ALL);
            break;
    
            case 'testing':
            case 'production':
                error_reporting(0);
            break;
    
            default:
                exit('The application environment is not set correctly.');
        }
    }


    Это код по стандарту PSR-2

    Код, написанный вами, не попадает под этот стандарт. Для примера:

    if (strlen($uri)>1) {// если не главная страница...
      if (rtrim($uri,'/')!=$uri) {
        header("HTTP/1.1 301 Moved Permanently");
        header('Location: http://'.$_SERVER['SERVER_NAME'].str_replace($uri, rtrim($uri,'/'), $_SERVER['REQUEST_URI']));
        exit();
      }
    }


    Я предлагаю самостоятельно определить, что здесь не соответствует PSR-2. В целом, я чаще всего забиваю на PSR-2. Но я пишу так, как написан основной код. Если основной код написан по стандарту PSR-2, я свои правки тоже делаю в этом стиле. Мешать стандарты, делать в одном месте отступ в два пробела, в другом — в четыре пробела, а в третьем в один TAB — это тоже признак юниорства. Сразу видно, что вы писали мало и не можете понять, что в коде должно быть единообразие, чтобы он выглядел не-юниорским.

    4. Велосипеды — ещё один признак новичка. Раз уж вы таким способом (как в листинге выше) пытаетесь определить главную страницу, можно сократить код (про «велосипед» объяснение в пунктах «в» и «г»):

    a) Нафига добавлять в блок IF единственный блок IF? Такая вложенность блоков нужна только если у вас внутри первого блока IF есть несколько IF-операций, а не одна единственная. Например:

    if (x = 0) {
        if ( isset(y) ) {
            echo 'The Y is set';
        }
    
        if ( isset(z) ) {
            echo 'The Z is set';
        }
    
        echo "Echo something";
    }


    Нужно так писать в вашем случае:

    if (strlen($uri) > 1 && rtrim($uri, '/') != $uri) {
        // some code
    }


    б) Если вы несколько раз пытаетесь использовать вызов функции с одними и теми же параметрами, и функция определённо возвращает один и тот же результат, лучше один раз присвоить её результат переменной и использовать переменную. Например, «rtrim($uri,'/')» вызывается несколько раз. Совершенно понятно, что она всё время будет возвращать один и тот же результат. Конечно, rtrim работает быстро и разницы не будет заметно в итоге. Но вот, например, если у вас функция будет делать запрос в БД, и вы будете её таким способом каждый раз дёргать, это скажется на производительности.

    Возьмите за правило: результат работы функции нужно присваивать переменной в том случае, если этот результат всё время один и тот же и будет использоваться в нескольких местах данной области видимости. С другой стороны, если переменная используется только один раз в данной области видимости, нет смысла её определять — достаточно в нужном месте просто вызвать код, возвращающий результат. Например:

    // Плохой подход
    echo 'У меня есть ' . get_my_apples_from_database() . ' яблок, потому что ' . get_my_apples_from_database() . ' яблок — это круто';
    
    // Хороший подход
    $my_apples = get_my_apples_from_database();
    echo "У меня есть $my_apples яблок, потому что $my_apples яблок — это круто";
    
    // Плохой подход
    $my_apples = get_my_apples_from_database();
    echo "У меня есть $my_apples яблок";
    
    // Хороший подход
    echo 'У меня есть ' . get_my_apples_from_database() . ' яблок';


    в) Если вы пользуетесь CodeIgniter, почему бы не пользоваться его возможностями определения факта, что сейчас открыта главная страница?

    г) Вобще, редиректы со слеша на страницу без слеша нужно делать средствами веб-сервера, а не средствами PHP-интерпретатора.

    Если у вас установлен Apache, можно добавить в .htaccess в корне веб-сайта такой код:

    RewriteCond %{REQUEST_URI} ^.+
    RewriteRule ^(.*)/$ /$1 [L,R=301]


    Это только для иллюстрации. Я не гарантирую, что эта конструкция будет работать идеально, но как пример она хорошо подходит.

    5. Вот такой интересный код в контроллере application/mvc/controllers/blog.php:
    $query = $this->db->query("SELECT count(`id`) FROM `blog_page` WHERE `category`='$id'");//Считаем


    CodeIgnitor — это MVC-фреймворк, он помогает понять, что такое MVC и как правильно писать MVC-приложения. Куча документации же на эту тему. Предлагается разделять код на модель, контроллер и представление. Это означает, что каждая часть выполняет свои какие-то функции. Модель, например, кроме прочего, занимается добычей данных из базы данных. Только модель этим должна заниматься. SQL-запросы в контроллере — это... есть такое выражение: «толстые тупые уродливые контроллеры». Это обычное определение, ничего обидного. Так называются контроллеры, которые делают запросы к БД и частично генерируют HTML-код. Они появляются в ситуации, когда разработчику влом прыгать по файлам и он начинает лепить всё в контроллер. Или когда у него нет достаточного количества знаний по MVC, чтобы правильно разделять код.

    * * *

    Вот. У вас куча таких вещей (и ещё дохрена вещей, которые можно покритиковать) в коде. Они показывают, что вы только недавно начали программировать, у вас небольшой «словарный запас», много велосипедов в коде, много неоптимальных подходов, вроде такого:

    $i = 0;
    
    foreach($query->result_array() as $row)
    {
    	$myrow[$i]['url'] = $row['url'];
    	$myrow[$i]['title'] = $row['title'];
    	$myrow[$i]['text'] = substr(strip_tags($row['text']),0,255);
    	$i++;
    }


    Вместо этого можно было написать так:

    $myrow[] = array();
    
    foreach($query->result_array() as $row)
    {
    	$myrow[] = array(
    		'url' => $row['url'],
    		'title' => $row['title'],
    		'text' => substr(strip_tags($row['text']),0,255)
    	);
    }


    Переменная $i тут не нужна. Можно нормально обойтись без неё. А вот $myrow обязательно нужно определить, чтобы не сталкиваться с неожиданными ошибками, если $query->result_array() вернёт 0 запией.

    Читайте разный код. Понятно, что это универсальный совет, на который вы ответите: «я знаю». Но если вы не будете читать, останетесь безграмотным, вне зависимости от того, знаете вы про полезность этого совета или нет.

    Если пишете на CodeIgniter, почитайте его исходники и документацию. Не почитаете — будете плодить велосипеды, вместо которых можно было бы использовать API фреймворка.

    Используете MVC? Почитайте, для начала: ru.wikipedia.org/wiki/Model-View-Controller Там про толстые контроллеры тоже есть.

    * * *

    Вообще, очень тонкий момент, когда заходит разговор про Junior'a, значит где-то рядом есть и Middle и Senior, значит вы хотите работать в конкретном месте с конкретными людьми. Есть смысл подоставать этих людей, набиться в ученики. Если вы так не думаете, пошлите их на три буквы и программируйте, как считаете нужным.

    DISCLAIMER: Я на всякий случай уточню, что не занимаюсь обучением или поиском чужих косяков в коде. У меня было немного свободного времени, которое я потратил на ответ. Это не значит, что я готов с кем-то няньчиться. Сам ничего не умею.
    Ответ написан
    2 комментария
  • Почему не работает xpath?

    nowm
    @nowm
    В первую очередь — из-за этого:

    $doc->load($str);

    «load» — это для загрузки файлов и в качестве параметра ей нужно давать путь к файлу. Если вы хотите загрузить строку, нужно использовать функцию «loadHTML».

    Дальше у вас появится куча предупреждений. Если появится сообщение про то, что непонятки с кодировкой появились, от него можно избавиться, если поправить строку с loadHTML:

    $doc->loadHTML(mb_convert_encoding($str, 'HTML-ENTITIES', 'UTF-8'));


    Кроме строки про кодировку будет ещё куча предупреждений, вроде:

    Warning: DOMDocument::loadHTML(): Opening and ending tag mismatch: li and div in Entity
    Warning: DOMDocument::loadHTML(): htmlParseEntityRef: expecting ';' in Entity
    Warning: DOMDocument::loadHTML(): Opening and ending tag mismatch: td and b in Entity


    Чтобы эти уведомления не засоряли эфир, можно добавить символ «@» при вызове «loadHTML»:

    @$doc->loadHTML($str);

    Дальше, чтобы удостовериться, что те узлы, которые вы пытаетесь искать, всё-таки существуют, можно вывести список вообще всех узлов, вот так:

    $res = $xpath->query('.//*');
    foreach($res as $obj) {
    	echo $obj->getNodePath() . "\n\r";
    }


    Из листинга будет видно, что упоминание связки «table/tbody/tr» некорректно. «TBODY» там нет. Такой XPath-запрос сработает нормально в FirePath из Firefox, например. И работает он из-за того, что Firefox самостоятельно достраивает DOM документа до идеального по его мнению состояния — например, добавляет «TBODY», где его нет, закрывает незакрытые теги и так далее.

    В ситуации с DomDocument и DomXPath лучше смотреть чистый исходный код страницы и строить запросы именно по исходному коду, а не по сгенерированному браузером DOM.

    В вашей ситуации нужно из запроса просто убрать «tbody». Получится такой запрос:

    //*[@id="content-all"]/div[2]/div/table/tr[3]

    Как я вижу, решение уже появилось, но, вообще, такой подход, который я описал, поможет искать ошибки в подобных ситуациях.
    Ответ написан
    3 комментария
  • Как правильно составить XPath?

    nowm
    @nowm
    С самого начала у вас должно быть понимание, что Query можно вызывать только для DomXPath, который инициализируется только с DomDocument. Всё. Ему нельзя подсовывать DomNodeList или DomNode. Только DomDocument. Из-за этого нужно применять другой подход к получению данных.

    Вы думаете, что можно найти запросом таблицу, потом ещё одним запросом найти в ней DIV, потом ещё одним запросом найти в этом DIV какой-нибудь SPAN, а в нём ещё одним запросом найти A. С DomXPath так нельзя работать. Хотите найти элемент, ищите его сразу — от корня DOM.

    Прямо сейчас могу что-то неточно написать в самих XPath-запросах, но делать нужно примерно так:

    $pageDom = new DOMDocument();
    @$pageDom->loadHTML($pageHtml);
    $pageXPath = new DomXPath($pageDom);
    
    $elementsName = $pageXPath->query('.//table/.//div[class="name"]');
    $elementsDescription = $pageXPath->query('.//table/.//div[class="description"]');
    $elementsRating = $pageXPath->query('.//table/.//div[class="rating"]');
    
    $elements = array();
    
    for ($i = 0; $i < $elementsName->length; $i++) {
        $elements[] = array(
            'name' => $elementsName->item($i)->nodeValue,
            'description' => $elementsDescription->item($i)->nodeValue,
            'rating' => $elementsRating->item($i)->nodeValue,
        );
    }
    
    //Profit


    Однако!

    Якориться к предыдущим результатам поиска всё-таки возможно. У функции DomXPath::query есть необязательный параметр с типом DOMNode. Получаются такие неявные под-запросы.

    $pageDom = new DOMDocument();
    @$pageDom->loadHTML($pageHtml);
    $pageXPath = new DomXPath($pageDom);
    
    $elementsDom = $pageXPath->query('.//table/tr');
    
    $elements = array();
    
    foreach ($elementsDom as $elementDom) {
        $elements[] = array(
            'name' => $pageXPath->query('.//div[class="name"]', $elementDom)->item(0)->nodeValue,
            'description' => $pageXPath->query('.//div[class="description"]', $elementDom)->item(0)->nodeValue,
            'rating' => $pageXPath->query('.//div[class="rating"]', $elementDom)->item(0)->nodeValue,
        );
    }


    Особенность в том, что используется всё тот же $pageXPath, а не происходит попытка создать из DOMNode отдельный DOMXPath. И дальше происходит поиск в контексте предыдущих результатов запроса — за счёт добавления в функцию DomXPath::query дополнительного параметра, уточняющего контекст, в котором происходит поиск — DomXPath::query(строка_запроса, контекст_поиска). Так что в такой ситуации «.//div[class="name"]» будет искаться не во всём документе, а только в текущей строке TR.
    Ответ написан
    3 комментария
  • Как передать значение переменной из php-скрипта в javascript?

    nowm
    @nowm
    Первый способ: Отдельный AJAX-запрос к PHP-файлу, который вернёт что-нибудь вроде JSON-данных, которые потом будут доступны из JS.

    Второй способ: В PHP-файле можно определить какую нибудь переменную во время генерации страницы, а потом из любых скриптов к ней обращаться. Например так:

    PHP-файл:
    <?php
    $a = 'text for js_variable';
    ?>
    <!DOCTYPE html>
    <html>
      <head>
        <script>
          //Определяется переменная, которая будет доступна для 
          // всех JavaScript, подключаемых на данной странице
          var js_variable = '<?php echo $a; ?>';
        </script>
        <!-- 
          В файле /scripts/myscript.js происходит обращение 
          к переменной js_variable 
        -->
        <script src="/scripts/myscript.js"></script>
      </head>
      <body>blah-blah-blah</body>
    </html>


    Файл /scripts/myscript.js:
    // Выскочит алерт с текстом «text for js_variable».
    alert(js_variable);


    Вот. В HEAD-части HTML-страницы вы определяете переменную, которая будет доступна для остального JS-кода. Главное — нужно её определять до того, как будут подключены скрипты, которые будут её использовать.

    Третий способ: Если ваш web-сервер — Apache, то можно добавить в .htaccess в корне сайта такие строки:
    AddType application/x-httpd-php .js
    AddHandler x-httpd-php5 .js
    
    <FilesMatch "\.js$">
    SetHandler application/x-httpd-php
    </FilesMatch>


    Тогда вы сможете прямо в JS-файлах выполнять PHP-код. Но выполняться он будет только в тех JS-файлах, которые находятся в папке с сайтом. Если будут загружаться скрипты со сторонних ресурсов, там PHP-код работать не будет.
    Ответ написан
    Комментировать
  • Связка Nginx+Apache. Странное поведение virtual hosts - почему?

    nowm
    @nowm
    Я сначала немного «теории» опишу, а в конце скажу, что в ваших конфигах нужно поправить.

    Вообще, у вас Apache не должен принимать никаких запросов от сторонних серверов. Связка Apache + Nginx делается потому, что Nginx очень хорошо отдаёт статичный контент, а Apache выигрывает в работе с PHP. К тому же, Apache даёт возможность использовать файлы .htaccess, которые позволяют очень гибко конфигурировать работу на уровне директорий одного сервера.

    Так вот. Суть связки Apache + Nginx в том, что Apache обрабатывается входящие запросы только от одного сервера. Обычно они оба ставятся на одной машине, и в этом случае Apache должен принимать запросы только от 127.0.0.1. Он не должен заботиться о том, чтобы обрабатывать имена dev1.site, dev2.site и так далее. Эта задача лежит на Nginx, который будет перенаправлять внешние запросы соответствующим образом.

    Для этого Apache конфигурируется таким образом, чтобы каждый его виртуальный хост слушал свой порт. Например, я строю всю систему таким образом, чтобы у меня было два сервера: task.site и dev.site. Для этого я делаю так:

    /etc/apache2/ports.com:
    NameVirtualHost 127.0.0.1:8080
    NameVirtualHost 127.0.0.1:8090
    Listen 127.0.0.1:8080
    Listen 127.0.0.1:8090


    Это значит, что у меня будет два инстанса, один из которых будет обслуживать имя dev.site, а другой — task.site (при этом, сам Apache как бы будет не в курсе, какой он домен обслуживает. Он знает только что запрос на порт 8080 нужно так обработать, а на 8090 — ещё как-то). Всё, что теперь от апача понадобится — сделать виртуальные хосты, чтобы определить папки, из которых будут вызываться скрипты для каждого из сайтов.

    /etc/apache2/sites-enabled/default.conf:
    <VirtualHost 127.0.0.1:8090>
    	. . .
    	DocumentRoot /usr/share/redmine/public
    	. . .
    </VirtualHost>
    <VirtualHost 127.0.0.1:8080>
    	. . .
    	DocumentRoot /var/www/dev.site
    	. . .
    </VirtualHost>


    Всё. Если запрос будет на 127.0.0.1:8080, то будут выполняться скрипты из папки /var/www/dev.site. Если на 127.0.0.1:8090, то — /usr/share/redmine/public. Сам Apache вообще не в курсе, какие он домены обслуживает, так как снаружи его не видно.

    Далее вступает в работу Nginx. Он отвечает за то, чтобы определить по имени сайта, куда отправлять запрос.

    /etc/nginx/sites-available/default.conf:
    server {
    	listen 80;
    	server_name dev.site;
    	server_name_in_redirect off;
    
    	location / {
    		proxy_pass http://127.0.0.1:8080/;
    		proxy_redirect off;
    		proxy_set_header Host $host;
    		proxy_set_header X-Real-IP $remote_addr;
    		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    		client_max_body_size 10m;
    		proxy_connect_timeout 90;
    	}
    }
    
    server {
    	listen 80;
    	server_name task.site;
    	server_name_in_redirect off;
    	
    	location / {
    		proxy_pass http://127.0.0.1:8090/;
    		proxy_redirect off;
    		proxy_set_header Host $host;
    		proxy_set_header X-Real-IP $remote_addr;
    		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    		client_max_body_size 10m;
    		proxy_connect_timeout 90;
    	}
    }


    Как видно из листинга, он для dev.site делает proxy_pass http://127.0.0.1:8080/, а для task.siteproxy_pass http://127.0.0.1:8090/.

    Теперь о том, что нужно поправить в ваших конфигах. В апаче нужно все сервера раскидать по отдельным портам. Сначала нужно прописать NameVirtualHost и Listen для каждого из них. При этом, обязательно нужно указывать не *:8080 или *:8081, *:8082, а конкретно 127.0.0.1:8080, 127.0.0.1:8081 и т.п., потому что «*» позволяет делать прямые запросы с любой машины, а «127.0.0.1» разрешает принимать соединения только с localhost.

    То же самое — в блоках VirtualHost. Вместо <VirtualHost *:8080> нужно писать <VirtualHost 127.0.0.1:8080>

    Из блока VirtualHost нужно выкинуть ServerName и ServerAlias, так как всё это будет обрабатываться на уровне Nginx.

    В итоге вы получите такую ситуацию, что напрямую у вас sub.myhost.ru:8080 открываться не будет, так как Apache принимает запросы только от localhost. Он будет открываться только по запросу от Nginx, который будет определять какой сервер куда проксировать.

    В вашем конфиге Nginx вместо server_name *.myhost.ru; нужно писать полное имя хоста. И для каждого хоста нужно создать отдельный блок server {}.

    В конфигах Nginx ещё не забудьте приписать обработку статики (хотя у вас она и так прописана). Я из листинга этот момент выкинул, но его вообще нужно добавить, потому что одна из прелестей Nginx — как раз обработка статики. В каждый блок server {} можно добавить что-нибудь вроде:

    location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|zip|tgz|gz|js|pdf)$ {
    	root /var/www/dev.site;
    }


    И далее, если вы хотите добавить подсервер: создаёте виртуальный хост апача на свободном порту, а в Nginx добавляете для него блок server {}.

    В данный момент у вас просто небольшая каша получилась в конфигах.
    Ответ написан
    8 комментариев
  • PHP: из-за чего странное поведение при статическом вызове метода?

    nowm
    @nowm
    Это такой небольшой глюк PHP. До тех пор, пока в функции foo() нет обращения к нестатическим переменным, вроде $this, её можно будет вызывать статическим способом.
    Ответ написан
    2 комментария
  • Стоит ли переходить ли на OpenCart с PHPShop?

    nowm
    @nowm
    В целом, насколько я вижу из своего опыта (я «переезжал» сайты с Bitrix, WebAsyst и Shop-Script), переезд на Опенкарт может обойтись примерно в 17-20 тысяч (миграция категорий/товаров/производителей/атрибутов/опций, создание/миграция шаблона).

    Сам движок вообще прикольный, хотя в нём до сих пор используется mysql-функции. Я недавно пытался сделать пулл-реквест, который нормально организовывал поддержку PDO с обратной совместимостью, но Даниэль меня, образно говоря, послал нахер (даже при условии, что несколько человек меня поддержали), и я что-то теперь разочаровался в этом движке, хотя целый год вообще работал только с ним — излазал вдоль и поперёк, делал кучу архитектурных усовершенствований для клиентов, писал модули.

    Сорри за лирическое отступление — я всё никак не найду, кому поплакаться в жилетку. :)

    Реально ли вообще построить на Open Cart серьезный проект?

    Да, на Opencart вполне возможно построить серьёзный магазин. У него большой плюс в том, что модули к нему можно очень легко и быстро писать. Даже, в принципе, начинающий программист может с этим справиться. И людей, которые что-то дорабатывают/пишут, очень много. Много всего можно найти на opencartforum.ru.

    С какими проблемами можно столкнуться в первое время?

    В первое время можно столкнуться с тем, что он будет тормозить на большом количестве продуктов и категорий. Он всё время пытается считать количество товаров в каждой категории, даже если в настройках это отключено. В официальный репозиторий больше года назад была принята правка, которая этот момент исправляет, но почему-то в последние версии эта правка так и не попала, как я вижу (и в v1.5.6 и в v1.5.6.1 эта недоработка всё ещё присутствует). Если этот момент исправить, скорость поразительно увеличивается. Я один раз умудрился $150 заработать на этом моменте — за пару минут работы. Там всего-то две небольшие правки сделать нужно.

    В остальном, движок особенно неприятных сюрпризов больше не доставляет.

    Какие могут быть размеры вложений для исправления основных проблем этой CMS?

    150 долларов — это была удача. Вообще, цена такой правки — примерно рублей 500, но вы наверняка можете и дешевле найти на профильных ресурсах. В MaxyStore, кажется, вообще этот момент поправлен. Так что он вполне нормально будет из коробки работать, и никаких основных проблем исправлять вообще не придётся.

    Оправданы ли мои опасения насчет того, что бесплатный продукт может принести слишком много проблем?

    Продукт продукту рознь. Сам Opencart особенно много проблем вам не принесёт. Но проблемы могут принести дополнительные модули. У Opencart довольно-таки низкий порог вхождения, поэтому качество кода может быть ужасным. Один модуль может начисто убить весь jQuery на странице. Другой сделает так, что у вас отзывы о продукте не будут отправляться. Третий (от какого-нибудь французского разработчика, который очень любит делать навигационные модули) будет на каждый пук дёргать базу данных, генерируя по 86 дополнительных запросов к БД на одной странице. Вообще, в ситуации с Опенкартом, лучше постоянно работать с одним и тем же программистом, который будет и модули устанавливать и дорабатывать и ошибки устранять и т.д. Мороки будет меньше, потому что на русскоязычных ресурсах расспрашивать, почему выскакивает такая-то и такая-то ошибка — неблагодарное дело.

    Есть проект MaxyStore на основе OpenCart - может стоит взглянуть в его сторону или нет смысла?

    Попробуйте ещё посмотреть в сторону ocStore с сайта opencartforum.ru. Мне кажется, что он немного лучше. С позиции разработчика мне лично MaxyStore не нравится. Я не могу внятно описать причины, но в ситуации, когда есть выбор между MaxyStore и Opencart, я однозначно выберу Opencart.

    У таких сборок, как MaxyStore и ocStore плюс в том, что они интегрируют много полезных модулей.

    Минусы. Они часто отстают по версиям на 1-2 релиза. К примеру, какой-нибудь новейший ***Store может быть основан на версии Opencart, которая вышла больше года назад.

    Зачастую финтиклюшки, которые разработчики сборок добавляют в ядро, замедляют работу движка. Как я вижу (может быть я не прав), разработчики движков на основе Opencart больше внимания уделяют внешнему виду, а не архитектурным вопросам, которые позволят повысить скорость или безопасность. А если они и начинают заниматься архитектурными вопросами, то движок частично теряет совместимость с модулями, написанными для оригинального Opencart.

    На какие еще CMS стоит обратить внимания, учитывая мои требования?

    Я вообще после того, как мой пул-реквест отшили, решил вообще больше не заниматься Opencart (такой вот я чувствительный) и перейти на разработку для Magento. Из всех движков, о которых я слышал (или которые щупал) он мне больше всего нравится. Хотя, вообще-то, я не так уж и много движков щупал.
    Ответ написан
    3 комментария
  • Как на php выполнить сразу несколько SQL запросов?

    nowm
    @nowm
    Первый способ: сохранить запросы в файл и импортировать этот файл через командную строку. mysql -u user database < /home/username/mysqldump.sql

    Второй способ: можно сократить количество запросов немного поменяв логику построения запроса. Вместо

    UPDATE `products` SET `status` = 1 WHERE `product_id` = 1;
    UPDATE `products` SET `status` = 1 WHERE `product_id` = 2;
    -- 
    UPDATE `products` SET `status` = 1 WHERE `product_id` = 1000;


    можно писать так:

    UPDATE `products` SET `status` = 1 WHERE `product_id` IN (1, 2, 3, 4, 5);
    UPDATE `products` SET `status` = 2 WHERE `product_id` IN (6, 7, 8, 9);


    Update: оу... я так медленно пишу. Когда начинал писать, ни одного ответа ещё не было. ))
    Ответ написан
    Комментировать
  • Как в SQL выбрать пользователей, у которых день рождения в ближайшую неделю?

    nowm
    @nowm
    Можно добавить в SQL-запрос условие:

    WHERE TIMESTAMPDIFF(DAY, `birthday`, NOW()) BETWEEN 0 AND 7

    WHERE TIMESTAMPDIFF(
        DAY, 
        DATE_ADD(`birthday`, INTERVAL YEAR(NOW())-YEAR(`birthday`) YEAR), 
        NOW()
    ) BETWEEN 0 AND 7

    (На несколько строк разбил только для того, чтобы было удобнее читать.)

    Спасибо @Kerman за подсказку — совсем не подумал про то, что нужно обратить внимание на год в `birthday`.
    Ответ написан
    Комментировать