├── .gitignore ├── LICENSE ├── README.md ├── bin └── console ├── composer.json ├── composer.lock ├── docker ├── .gitignore ├── build │ ├── nginx │ │ ├── Dockerfile │ │ ├── nginx.conf │ │ └── sites-enabled │ │ │ └── ws-chat.conf │ ├── php │ │ ├── Dockerfile │ │ ├── docker-php-entrypoint │ │ ├── docker-php-ext-configure │ │ ├── docker-php-ext-enable │ │ ├── docker-php-ext-install │ │ ├── docker-php-source │ │ └── systemd │ │ │ └── app.target │ └── workspace │ │ └── Dockerfile ├── docker-compose.yml ├── env-example └── shortcuts │ ├── exec_db │ ├── exec_php │ └── exec_workspace ├── mix-manifest.json ├── output ├── alias │ ├── Client.php │ ├── Http │ │ ├── Client.php │ │ ├── Request.php │ │ ├── Response.php │ │ └── Server.php │ ├── Http2 │ │ ├── Client.php │ │ └── Request.php │ ├── Mysql.php │ ├── Mysql │ │ ├── Exception.php │ │ └── Statement.php │ ├── Redis.php │ ├── Redis │ │ └── Server.php │ ├── Server.php │ ├── Server │ │ └── Port.php │ ├── Socket.php │ ├── Socket │ │ └── Exception.php │ └── Websocket │ │ ├── Frame.php │ │ └── Server.php ├── classes.php ├── constants.php ├── functions.php └── namespace │ ├── Async.php │ ├── Atomic.php │ ├── Atomic │ └── Long.php │ ├── Buffer.php │ ├── Channel.php │ ├── Client.php │ ├── Connection │ └── Iterator.php │ ├── Coro │ └── Channel.php │ ├── Coroutine.php │ ├── Coroutine │ ├── Channel.php │ ├── Client.php │ ├── Http │ │ └── Client.php │ ├── Http2 │ │ ├── Client.php │ │ └── Request.php │ ├── MySQL.php │ ├── MySQL │ │ ├── Exception.php │ │ └── Statement.php │ ├── Redis.php │ ├── Socket.php │ └── Socket │ │ └── Exception.php │ ├── Event.php │ ├── Exception.php │ ├── Http │ ├── Client.php │ ├── Request.php │ ├── Response.php │ └── Server.php │ ├── Http2 │ ├── Client.php │ ├── Request.php │ └── Response.php │ ├── Lock.php │ ├── Memory │ ├── Pool.php │ └── Pool │ │ └── Slice.php │ ├── Mmap.php │ ├── MsgQueue.php │ ├── MySQL.php │ ├── MySQL │ └── Exception.php │ ├── Mysql.php │ ├── Mysql │ ├── Exception.php │ └── Statement.php │ ├── Process.php │ ├── Process │ └── Pool.php │ ├── Redis.php │ ├── Redis │ └── Server.php │ ├── RingQueue.php │ ├── Serialize.php │ ├── Server.php │ ├── Server │ └── Port.php │ ├── Socket.php │ ├── Socket │ └── Exception.php │ ├── Table.php │ ├── Table │ └── Row.php │ ├── Timer.php │ ├── WebSocket │ ├── Frame.php │ └── Server.php │ └── Websocket │ ├── Frame.php │ └── Server.php ├── package-lock.json ├── package.json ├── psalm.xml ├── public └── index.php ├── resources ├── css │ └── app.scss └── js │ ├── app.js │ ├── components │ ├── Chatbox.vue │ ├── MessagesView.vue │ └── UsersTable.vue │ └── services │ └── ChatClientRequestBuilder.js ├── src ├── Commands │ ├── CreateDatabaseCommand.php │ ├── CreateMigrationCommand.php │ ├── MigrateCommand.php │ └── StartAppCommand.php ├── Config.php.sample ├── Helper │ ├── DatabaseHelper.php │ ├── PurifierHelper.php │ ├── RequestLimiter.php │ └── SpamFilter.php ├── Message.php ├── Migration │ └── Version20190723174016730.php ├── MigrationsComponent │ ├── Internal │ │ ├── DBCurrentSchemaVersionProvider.php │ │ ├── MigrationExecutor.php │ │ ├── MigrationsLoader.php │ │ ├── MigratorException.php │ │ ├── MigratorResultText.php │ │ └── SchemaUpdater.php │ ├── Migration.php │ └── Migrator.php ├── Repository │ ├── MessagesRepository.php │ └── UsersRepository.php ├── Request │ ├── LoginRequest.php │ └── MessageRequest.php ├── Response │ ├── ErrorJsonReponse.php │ ├── JsonReponse.php │ ├── LoginJsonReponse.php │ ├── MessagesJsonReponse.php │ └── UsersJsonReponse.php ├── User.php └── WebsocketServer.php └── webpack.mix.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | app/DBConfig.php 4 | /node_modules 5 | /public/css 6 | /public/js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Evgeniy Romashkan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

This repository is deprecated and will not be supported by a bunch of reasons, including changing my views on solving such problems, poor code quality and the availability of documentation for the Swoole framework

2 | 3 |
4 | 5 | Online chat on websocket using Swoole (https://www.swoole.co.uk/) 6 | 7 | For using on local machine: 8 | 1. Make sure you have installed Docker and Docker-compose 9 | 2. Clone project using Git and enter project directory 10 | 3. cp src/Config.php.sample src/Config.php and configure it. 11 | 12 | cp ./docker/env-example ./docker/.env and configure it. 13 | 4. cd docker; docker-compose build && docker-compose up -d 14 | 5. docker exec -it ws_chat_workspace bash 15 | 16 | npm install 17 | 18 | composer install 19 | 20 | php bin/console app:database:create 21 | 22 | php bin/console app:migration:migrate (Enter nothing when it`ll ask about the version) 23 | 24 | npm run production 25 | 26 | (May be someday I will automate this process) 27 | 28 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | add(new StartAppCommand()); 12 | $application->add(new CreateMigrationCommand()); 13 | $application->add(new CreateDatabaseCommand()); 14 | $application->add(new MigrateCommand()); 15 | 16 | $application->run(); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root/ws-chat", 3 | "require": { 4 | "ext-json": "*", 5 | "ezyang/htmlpurifier": "^4.10", 6 | "ext-pdo": "*", 7 | "symfony/console": "^4.3" 8 | }, 9 | "autoload": { 10 | "psr-4": { 11 | "App\\": "src/" 12 | } 13 | }, 14 | "require-dev": { 15 | "vimeo/psalm": "^3.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docker/.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | postgres/ 3 | .env 4 | -------------------------------------------------------------------------------- /docker/build/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stretch 2 | RUN apt-get update && apt-get install -y nginx vim nano 3 | COPY nginx.conf /etc/nginx/nginx.conf 4 | ADD sites-enabled /etc/nginx/sites-enabled 5 | RUN rm /etc/nginx/sites-enabled/default 6 | WORKDIR /var/www/html/symfony 7 | EXPOSE 80 8 | EXPOSE 443 9 | CMD ["nginx"] 10 | -------------------------------------------------------------------------------- /docker/build/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | pid /run/nginx.pid; 3 | worker_processes 1; 4 | daemon off; 5 | events { 6 | worker_connections 768; 7 | # multi_accept on; 8 | } 9 | http { 10 | client_max_body_size 20M; 11 | ## 12 | # Basic Settings 13 | ## 14 | sendfile on; 15 | tcp_nopush on; 16 | tcp_nodelay on; 17 | keepalive_timeout 65; 18 | types_hash_max_size 2048; 19 | # server_tokens off; 20 | # server_names_hash_bucket_size 64; 21 | # server_name_in_redirect off; 22 | include /etc/nginx/mime.types; 23 | default_type application/octet-stream; 24 | ## 25 | # SSL Settings 26 | ## 27 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE 28 | ssl_prefer_server_ciphers on; 29 | ## 30 | # Logging Settings 31 | ## 32 | access_log /var/log/nginx/access.log; 33 | error_log /var/log/nginx/error.log; 34 | ## 35 | # Gzip Settings 36 | ## 37 | gzip on; 38 | gzip_disable "msie6"; 39 | gzip_vary on; 40 | gzip_proxied any; 41 | gzip_comp_level 6; 42 | gzip_buffers 16 8k; 43 | gzip_http_version 1.1; 44 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; 45 | ## 46 | # Virtual Host Configs 47 | ## 48 | include /etc/nginx/conf.d/*.conf; 49 | include /etc/nginx/sites-enabled/*; 50 | } -------------------------------------------------------------------------------- /docker/build/nginx/sites-enabled/ws-chat.conf: -------------------------------------------------------------------------------- 1 | server { 2 | server_name localhost; 3 | root /var/www/html/ws-chat/public; 4 | 5 | location / { 6 | # try to serve file directly, fallback to index.php 7 | try_files $uri /index.php$is_args$args; 8 | } 9 | 10 | location /ws { 11 | proxy_pass http://ws:9502; 12 | proxy_http_version 1.1; 13 | proxy_set_header Upgrade $http_upgrade; 14 | proxy_set_header Connection "Upgrade"; 15 | } 16 | # DEV 17 | # This rule swoole_websocket_servershould only be placed on your development environment 18 | # In production, don't include this and don't deploy index_dev.php or config.php 19 | #location ~ ^/(index_dev|config)\.php(/|$) { 20 | # fastcgi_pass php:9000; 21 | # fastcgi_split_path_info ^(.+\.php)(/.*)$; 22 | # include fastcgi_params; 23 | # When you are using symlinks to link the document root to the 24 | # current version of your application, you should pass the real 25 | # application path instead of the path to the symlink to PHP 26 | # FPM. 27 | # Otherwise, PHP's OPcache may not properly detect changes to 28 | # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126 29 | # for more information). 30 | # fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 31 | # fastcgi_param DOCUMENT_ROOT $realpath_root; 32 | #} 33 | # PROD 34 | location ~ ^/index\.php(/|$) { 35 | # php-fpm mean container name 36 | fastcgi_pass php-fpm:9000; 37 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 38 | include fastcgi_params; 39 | # When you are using symlinks to link the document root to the 40 | # current version of your application, you should pass the real 41 | # application path instead of the path to the symlink to PHP 42 | # FPM. 43 | # Otherwise, PHP's OPcache may not properly detect changes to 44 | # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126 45 | # for more information). 46 | fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 47 | fastcgi_param DOCUMENT_ROOT $realpath_root; 48 | # Prevents URIs that include the front controller. This will 404: 49 | # http://domain.tld/index.php/some-path 50 | # Remove the internal directive to allow URIs like this 51 | internal; 52 | } 53 | # return 404 for all other php files not matching the front controller 54 | # this prevents access to other php files you don't want to be accessible. 55 | location ~ \.php$ { 56 | return 404; 57 | } 58 | error_log /var/log/nginx/error.log; 59 | access_log /var/log/nginx/access.log; 60 | } 61 | -------------------------------------------------------------------------------- /docker/build/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.2-fpm 2 | 3 | RUN apt-get update 4 | 5 | RUN set -ex && \ 6 | apt-get install vim -y && \ 7 | apt-get install openssl -y && \ 8 | apt-get install libssl-dev -y && \ 9 | apt-get install wget -y && \ 10 | apt-get install libpq-dev -y 11 | 12 | RUN cd /tmp && wget https://pecl.php.net/get/swoole-4.2.9.tgz && \ 13 | tar zxvf swoole-4.2.9.tgz && \ 14 | cd swoole-4.2.9 && \ 15 | phpize && \ 16 | ./configure --enable-openssl && \ 17 | make && make install 18 | 19 | RUN touch /usr/local/etc/php/conf.d/swoole.ini && \ 20 | echo 'extension=swoole.so' > /usr/local/etc/php/conf.d/swoole.ini 21 | 22 | RUN docker-php-ext-install pdo_pgsql 23 | 24 | WORKDIR /var/www/html/ws-chat 25 | 26 | EXPOSE 8101 -------------------------------------------------------------------------------- /docker/build/php/docker-php-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # first arg is `-f` or `--some-option` 5 | if [ "${1#-}" != "$1" ]; then 6 | set -- php-fpm "$@" 7 | fi 8 | 9 | exec "$@" 10 | -------------------------------------------------------------------------------- /docker/build/php/docker-php-ext-configure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # prefer user supplied CFLAGS, but default to our PHP_CFLAGS 5 | : ${CFLAGS:=$PHP_CFLAGS} 6 | : ${CPPFLAGS:=$PHP_CPPFLAGS} 7 | : ${LDFLAGS:=$PHP_LDFLAGS} 8 | export CFLAGS CPPFLAGS LDFLAGS 9 | 10 | srcExists= 11 | if [ -d /usr/src/php ]; then 12 | srcExists=1 13 | fi 14 | docker-php-source extract 15 | if [ -z "$srcExists" ]; then 16 | touch /usr/src/php/.docker-delete-me 17 | fi 18 | 19 | cd /usr/src/php/ext 20 | 21 | usage() { 22 | echo "usage: $0 ext-name [configure flags]" 23 | echo " ie: $0 gd --with-jpeg-dir=/usr/local/something" 24 | echo 25 | echo 'Possible values for ext-name:' 26 | find . \ 27 | -mindepth 2 \ 28 | -maxdepth 2 \ 29 | -type f \ 30 | -name 'config.m4' \ 31 | | xargs -n1 dirname \ 32 | | xargs -n1 basename \ 33 | | sort \ 34 | | xargs 35 | echo 36 | echo 'Some of the above modules are already compiled into PHP; please check' 37 | echo 'the output of "php -i" to see which modules are already loaded.' 38 | } 39 | 40 | ext="$1" 41 | if [ -z "$ext" ] || [ ! -d "$ext" ]; then 42 | usage >&2 43 | exit 1 44 | fi 45 | shift 46 | 47 | pm='unknown' 48 | if [ -e /lib/apk/db/installed ]; then 49 | pm='apk' 50 | fi 51 | 52 | if [ "$pm" = 'apk' ]; then 53 | if \ 54 | [ -n "$PHPIZE_DEPS" ] \ 55 | && ! apk info --installed .phpize-deps > /dev/null \ 56 | && ! apk info --installed .phpize-deps-configure > /dev/null \ 57 | ; then 58 | apk add --no-cache --virtual .phpize-deps-configure $PHPIZE_DEPS 59 | fi 60 | fi 61 | 62 | if command -v dpkg-architecture > /dev/null; then 63 | gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)" 64 | set -- --build="$gnuArch" "$@" 65 | fi 66 | 67 | cd "$ext" 68 | phpize 69 | ./configure "$@" 70 | -------------------------------------------------------------------------------- /docker/build/php/docker-php-ext-enable: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | extDir="$(php -r 'echo ini_get("extension_dir");')" 5 | cd "$extDir" 6 | 7 | usage() { 8 | echo "usage: $0 [options] module-name [module-name ...]" 9 | echo " ie: $0 gd mysqli" 10 | echo " $0 pdo pdo_mysql" 11 | echo " $0 --ini-name 0-apc.ini apcu apc" 12 | echo 13 | echo 'Possible values for module-name:' 14 | find -maxdepth 1 \ 15 | -type f \ 16 | -name '*.so' \ 17 | -exec basename '{}' ';' \ 18 | | sort \ 19 | | xargs 20 | echo 21 | echo 'Some of the above modules are already compiled into PHP; please check' 22 | echo 'the output of "php -i" to see which modules are already loaded.' 23 | } 24 | 25 | opts="$(getopt -o 'h?' --long 'help,ini-name:' -- "$@" || { usage >&2 && false; })" 26 | eval set -- "$opts" 27 | 28 | iniName= 29 | while true; do 30 | flag="$1" 31 | shift 32 | case "$flag" in 33 | --help|-h|'-?') usage && exit 0 ;; 34 | --ini-name) iniName="$1" && shift ;; 35 | --) break ;; 36 | *) 37 | { 38 | echo "error: unknown flag: $flag" 39 | usage 40 | } >&2 41 | exit 1 42 | ;; 43 | esac 44 | done 45 | 46 | modules= 47 | for module; do 48 | if [ -z "$module" ]; then 49 | continue 50 | fi 51 | if [ -f "$module.so" ] && ! [ -f "$module" ]; then 52 | # allow ".so" to be optional 53 | module="$module.so" 54 | fi 55 | if ! [ -f "$module" ]; then 56 | echo >&2 "error: '$module' does not exist" 57 | echo >&2 58 | usage >&2 59 | exit 1 60 | fi 61 | modules="$modules $module" 62 | done 63 | 64 | if [ -z "$modules" ]; then 65 | usage >&2 66 | exit 1 67 | fi 68 | 69 | pm='unknown' 70 | if [ -e /lib/apk/db/installed ]; then 71 | pm='apk' 72 | fi 73 | 74 | apkDel= 75 | if [ "$pm" = 'apk' ]; then 76 | if \ 77 | [ -n "$PHPIZE_DEPS" ] \ 78 | && ! apk info --installed .phpize-deps > /dev/null \ 79 | && ! apk info --installed .phpize-deps-configure > /dev/null \ 80 | ; then 81 | apk add --no-cache --virtual '.docker-php-ext-enable-deps' binutils 82 | apkDel='.docker-php-ext-enable-deps' 83 | fi 84 | fi 85 | 86 | for module in $modules; do 87 | if readelf --wide --syms "$module" | grep -q ' zend_extension_entry$'; then 88 | # https://wiki.php.net/internals/extensions#loading_zend_extensions 89 | absModule="$(readlink -f "$module")" 90 | line="zend_extension=$absModule" 91 | else 92 | line="extension=$module" 93 | fi 94 | 95 | ext="$(basename "$module")" 96 | ext="${ext%.*}" 97 | if php -r 'exit(extension_loaded("'"$ext"'") ? 0 : 1);'; then 98 | # this isn't perfect, but it's better than nothing 99 | # (for example, 'opcache.so' presents inside PHP as 'Zend OPcache', not 'opcache') 100 | echo >&2 101 | echo >&2 "warning: $ext ($module) is already loaded!" 102 | echo >&2 103 | continue 104 | fi 105 | 106 | ini="$PHP_INI_DIR/conf.d/${iniName:-"docker-php-ext-$ext.ini"}" 107 | if ! grep -q "$line" "$ini" 2>/dev/null; then 108 | echo "$line" >> "$ini" 109 | fi 110 | done 111 | 112 | if [ "$pm" = 'apk' ] && [ -n "$apkDel" ]; then 113 | apk del --no-network $apkDel 114 | fi 115 | -------------------------------------------------------------------------------- /docker/build/php/docker-php-ext-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # prefer user supplied CFLAGS, but default to our PHP_CFLAGS 5 | : ${CFLAGS:=$PHP_CFLAGS} 6 | : ${CPPFLAGS:=$PHP_CPPFLAGS} 7 | : ${LDFLAGS:=$PHP_LDFLAGS} 8 | export CFLAGS CPPFLAGS LDFLAGS 9 | 10 | srcExists= 11 | if [ -d /usr/src/php ]; then 12 | srcExists=1 13 | fi 14 | docker-php-source extract 15 | if [ -z "$srcExists" ]; then 16 | touch /usr/src/php/.docker-delete-me 17 | fi 18 | 19 | cd /usr/src/php/ext 20 | 21 | usage() { 22 | echo "usage: $0 [-jN] ext-name [ext-name ...]" 23 | echo " ie: $0 gd mysqli" 24 | echo " $0 pdo pdo_mysql" 25 | echo " $0 -j5 gd mbstring mysqli pdo pdo_mysql shmop" 26 | echo 27 | echo 'if custom ./configure arguments are necessary, see docker-php-ext-configure' 28 | echo 29 | echo 'Possible values for ext-name:' 30 | find . \ 31 | -mindepth 2 \ 32 | -maxdepth 2 \ 33 | -type f \ 34 | -name 'config.m4' \ 35 | | xargs -n1 dirname \ 36 | | xargs -n1 basename \ 37 | | sort \ 38 | | xargs 39 | echo 40 | echo 'Some of the above modules are already compiled into PHP; please check' 41 | echo 'the output of "php -i" to see which modules are already loaded.' 42 | } 43 | 44 | opts="$(getopt -o 'h?j:' --long 'help,jobs:' -- "$@" || { usage >&2 && false; })" 45 | eval set -- "$opts" 46 | 47 | j=1 48 | while true; do 49 | flag="$1" 50 | shift 51 | case "$flag" in 52 | --help|-h|'-?') usage && exit 0 ;; 53 | --jobs|-j) j="$1" && shift ;; 54 | --) break ;; 55 | *) 56 | { 57 | echo "error: unknown flag: $flag" 58 | usage 59 | } >&2 60 | exit 1 61 | ;; 62 | esac 63 | done 64 | 65 | exts= 66 | for ext; do 67 | if [ -z "$ext" ]; then 68 | continue 69 | fi 70 | if [ ! -d "$ext" ]; then 71 | echo >&2 "error: $PWD/$ext does not exist" 72 | echo >&2 73 | usage >&2 74 | exit 1 75 | fi 76 | exts="$exts $ext" 77 | done 78 | 79 | if [ -z "$exts" ]; then 80 | usage >&2 81 | exit 1 82 | fi 83 | 84 | pm='unknown' 85 | if [ -e /lib/apk/db/installed ]; then 86 | pm='apk' 87 | fi 88 | 89 | apkDel= 90 | if [ "$pm" = 'apk' ]; then 91 | if [ -n "$PHPIZE_DEPS" ]; then 92 | if apk info --installed .phpize-deps-configure > /dev/null; then 93 | apkDel='.phpize-deps-configure' 94 | elif ! apk info --installed .phpize-deps > /dev/null; then 95 | apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS 96 | apkDel='.phpize-deps' 97 | fi 98 | fi 99 | fi 100 | 101 | popDir="$PWD" 102 | for ext in $exts; do 103 | cd "$ext" 104 | [ -e Makefile ] || docker-php-ext-configure "$ext" 105 | make -j"$j" 106 | make -j"$j" install 107 | find modules \ 108 | -maxdepth 1 \ 109 | -name '*.so' \ 110 | -exec basename '{}' ';' \ 111 | | xargs -r docker-php-ext-enable 112 | make -j"$j" clean 113 | cd "$popDir" 114 | done 115 | 116 | if [ "$pm" = 'apk' ] && [ -n "$apkDel" ]; then 117 | apk del --no-network $apkDel 118 | fi 119 | 120 | if [ -e /usr/src/php/.docker-delete-me ]; then 121 | docker-php-source delete 122 | fi 123 | -------------------------------------------------------------------------------- /docker/build/php/docker-php-source: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | dir=/usr/src/php 5 | 6 | usage() { 7 | echo "usage: $0 COMMAND" 8 | echo 9 | echo "Manage php source tarball lifecycle." 10 | echo 11 | echo "Commands:" 12 | echo " extract extract php source tarball into directory $dir if not already done." 13 | echo " delete delete extracted php source located into $dir if not already done." 14 | echo 15 | } 16 | 17 | case "$1" in 18 | extract) 19 | mkdir -p "$dir" 20 | if [ ! -f "$dir/.docker-extracted" ]; then 21 | tar -Jxf /usr/src/php.tar.xz -C "$dir" --strip-components=1 22 | touch "$dir/.docker-extracted" 23 | fi 24 | ;; 25 | 26 | delete) 27 | rm -rf "$dir" 28 | ;; 29 | 30 | *) 31 | usage 32 | exit 1 33 | ;; 34 | esac 35 | -------------------------------------------------------------------------------- /docker/build/php/systemd/app.target: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=App 3 | Type=simple 4 | PIDFile=/work/www/myunit/shared/tmp/pids/service.pid -------------------------------------------------------------------------------- /docker/build/workspace/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copied & Pasted from Laradock https://github.com/laradock 2 | # 3 | #-------------------------------------------------------------------------- 4 | # Image Setup 5 | #-------------------------------------------------------------------------- 6 | # 7 | 8 | FROM phusion/baseimage:0.11 9 | 10 | LABEL maintainer="Mahmoud Zalt " 11 | 12 | RUN DEBIAN_FRONTEND=noninteractive 13 | RUN locale-gen en_US.UTF-8 14 | 15 | ENV LANGUAGE=en_US.UTF-8 16 | ENV LC_ALL=en_US.UTF-8 17 | ENV LC_CTYPE=en_US.UTF-8 18 | ENV LANG=en_US.UTF-8 19 | ENV TERM xterm 20 | 21 | # Add the "PHP 7" ppa 22 | RUN apt-get install -y software-properties-common && \ 23 | add-apt-repository -y ppa:ondrej/php 24 | 25 | RUN groupadd --system user --gid 1000 && useradd --no-log-init --system --gid 1000 --uid 1000 user 26 | 27 | # 28 | #-------------------------------------------------------------------------- 29 | # Software's Installation 30 | #-------------------------------------------------------------------------- 31 | # 32 | 33 | # Install "PHP Extentions", "libraries", "Software's" 34 | RUN apt-get update && \ 35 | apt-get upgrade -y && \ 36 | curl -sL https://deb.nodesource.com/setup_12.x | bash - && \ 37 | apt-get install -y --allow-downgrades --allow-remove-essential \ 38 | --allow-change-held-packages \ 39 | php7.3-cli \ 40 | php7.3-common \ 41 | php7.3-curl \ 42 | php7.3-intl \ 43 | php7.3-json \ 44 | php7.3-xml \ 45 | php7.3-mbstring \ 46 | php7.3-mysql \ 47 | php7.3-pgsql \ 48 | php7.3-sqlite \ 49 | php7.3-sqlite3 \ 50 | php7.3-zip \ 51 | php7.3-bcmath \ 52 | php7.3-memcached \ 53 | php7.3-gd \ 54 | php7.3-dev \ 55 | nodejs \ 56 | pkg-config \ 57 | libcurl4-openssl-dev \ 58 | libedit-dev \ 59 | libssl-dev \ 60 | libxml2-dev \ 61 | xz-utils \ 62 | libsqlite3-dev \ 63 | sqlite3 \ 64 | git \ 65 | curl \ 66 | vim \ 67 | nano \ 68 | postgresql-client \ 69 | && apt-get clean 70 | 71 | ##################################### 72 | # Composer: 73 | ##################################### 74 | 75 | # Install composer and add its bin to the PATH. 76 | RUN curl -s http://getcomposer.org/installer | php && \ 77 | echo "export PATH=${PATH}:/var/www/vendor/bin" >> ~/.bashrc && \ 78 | mv composer.phar /usr/local/bin/composer 79 | 80 | WORKDIR /var/www/html/ws-chat 81 | 82 | # Source the bash 83 | RUN sh 84 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | ws: 4 | build: build/php 5 | container_name: ws_chat_ws 6 | expose: 7 | - '9502' 8 | depends_on: 9 | - db 10 | volumes: 11 | - ./../:/var/www/html/ws-chat:cached 12 | - ./logs:/var/log 13 | command: php /var/www/html/ws-chat/bin/console app:start 14 | php-fpm: 15 | build: build/php 16 | container_name: ws_chat_fpm 17 | expose: 18 | - '9000' 19 | depends_on: 20 | - db 21 | volumes: 22 | - ./../:/var/www/html/ws-chat:cached 23 | - ./logs:/var/log 24 | db: 25 | image: postgres:11.2 26 | restart: always 27 | container_name: ws_chat_db 28 | environment: 29 | POSTGRES_PASSWORD: ${DB_PASS} 30 | POSTGRES_USER: ${DB_USER} 31 | volumes: 32 | - ./postgres:/var/lib/postgresql/data 33 | workspace: 34 | build: build/workspace 35 | restart: always 36 | container_name: ws_chat_workspace 37 | depends_on: 38 | - db 39 | volumes: 40 | - ./../:/var/www/html/ws-chat:cached 41 | - ./logs:/var/log/nginx 42 | nginx: 43 | build: build/nginx 44 | restart: always 45 | container_name: ws_chat_nginx 46 | ports: 47 | - 81:80 48 | depends_on: 49 | - php-fpm 50 | - db 51 | volumes: 52 | - ./../:/var/www/html/ws-chat:cached 53 | - ./logs:/var/log/nginx -------------------------------------------------------------------------------- /docker/env-example: -------------------------------------------------------------------------------- 1 | # cp ./env-example ./.env 2 | DB_USER=test 3 | DB_PASS=test 4 | PHP_INI_DIR=/usr/local/etc/php 5 | -------------------------------------------------------------------------------- /docker/shortcuts/exec_db: -------------------------------------------------------------------------------- 1 | docker exec -it ws_chat_db bash 2 | -------------------------------------------------------------------------------- /docker/shortcuts/exec_php: -------------------------------------------------------------------------------- 1 | docker exec -it -u $(id -u):$(id -g) ws_chat_php sh 2 | -------------------------------------------------------------------------------- /docker/shortcuts/exec_workspace: -------------------------------------------------------------------------------- 1 | docker exec -it -u user ws_chat_workspace bash -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/public/js/app.js": "/public/js/app.js", 3 | "/public/css/app.css": "/public/css/app.css" 4 | } 5 | -------------------------------------------------------------------------------- /output/alias/Client.php: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | ws-chat 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/css/app.scss: -------------------------------------------------------------------------------- 1 | @import '~bootstrap/scss/bootstrap'; 2 | 3 | #chatbox{ 4 | overflow-y: scroll; 5 | height: 80vh; 6 | } 7 | 8 | #chatbox div{ 9 | overflow: hidden; 10 | } 11 | 12 | #chatbox div span{ 13 | word-break: break-all; 14 | } 15 | 16 | #users-table { 17 | width: 100%; 18 | border-spacing: 7px 5px; 19 | } 20 | 21 | #users-table td { 22 | border: 0.1px solid #333; 23 | padding: 5px; 24 | } -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | require('bootstrap'); 2 | 3 | window.Vue = require('vue'); 4 | 5 | Vue.component('messages-view', require('./components/MessagesView.vue')); 6 | Vue.component('chatbox', require('./components/Chatbox.vue')); 7 | Vue.component('users-table', require('./components/UsersTable.vue')); 8 | 9 | const app = new Vue({ 10 | el: '#app' 11 | }); -------------------------------------------------------------------------------- /resources/js/components/Chatbox.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /resources/js/components/MessagesView.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 69 | -------------------------------------------------------------------------------- /resources/js/components/UsersTable.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /resources/js/services/ChatClientRequestBuilder.js: -------------------------------------------------------------------------------- 1 | export default class ChatClientRequestBuilder { 2 | constructor(websocket) { 3 | this.websocket = websocket; 4 | this.queryFields = {}; 5 | } 6 | 7 | setField(key, value) { 8 | this.queryFields[key] = value; 9 | } 10 | 11 | login(username) { 12 | this.setField('type', 'login'); 13 | this.setField('username', username); 14 | this.sendRequest(); 15 | } 16 | 17 | sendMessage(message) { 18 | this.setField('type', 'message'); 19 | this.setField('message', message); 20 | this.sendRequest(); 21 | } 22 | 23 | sendRequest() { 24 | this.websocket.send(JSON.stringify(this.queryFields)); 25 | return true; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Commands/CreateDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | setDescription(<<<'TEXT' 18 | Run to create database when installing the project. Command is idempotent and shouldn`t harm or clean up an existing database by accidental start-up. 19 | TEXT 20 | ); 21 | } 22 | 23 | protected function execute(InputInterface $input, OutputInterface $output) 24 | { 25 | $dsn = 'pgsql:host=' . Config::HOST; 26 | $pdo = new PDO($dsn, Config::USER, Config::PASSWORD, Config::OPT); 27 | $db = Config::DATABASE; 28 | 29 | $dbCount = $pdo->query("SELECT datname FROM pg_catalog.pg_database WHERE lower(datname) = lower('$db')")->rowCount(); 30 | if ($dbCount === 0) { 31 | $res = $pdo->exec("CREATE DATABASE \"$db\";"); 32 | if ($res === false) { 33 | $output->writeln(print_r($pdo->errorInfo(), true)); 34 | } else { 35 | $output->writeln('Success.'); 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Commands/CreateMigrationCommand.php: -------------------------------------------------------------------------------- 1 | setDescription(<<<'TEXT' 17 | Creates new migration in migrations folder 18 | TEXT 19 | ); 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output) 23 | { 24 | $migrationsFolderPath = __DIR__ . '/../Migration/'; 25 | $migrationsNamespace = 'App\Migration'; 26 | 27 | $dateTimeString = (new DateTimeImmutable())->format('YmdHisv'); 28 | 29 | $className = 'Version' . $dateTimeString; 30 | 31 | $migrationTemplate = <<<"TEMPLATE" 32 | writeln('Success'); 58 | } 59 | } -------------------------------------------------------------------------------- /src/Commands/MigrateCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Execute a migration to a specified version or the latest available version.'); 21 | } 22 | 23 | /** 24 | * @throws \App\MigrationsComponent\Internal\MigratorException 25 | * @throws \ReflectionException 26 | * @throws \Exception 27 | */ 28 | protected function execute(InputInterface $input, OutputInterface $output) 29 | { 30 | $dsn = 'pgsql:host=' . Config::HOST . ';dbname=' . Config::DATABASE; 31 | $pdo = new PDO($dsn, Config::USER, Config::PASSWORD, Config::OPT); 32 | $migrationsFolderPath = Config::SRCDIR . '/Migration/'; 33 | $migrator = new Migrator($pdo, $migrationsFolderPath); 34 | $result = $migrator->updateSchema($this->askVersionUserWant($input, $output)); 35 | $output->writeln($result); 36 | } 37 | 38 | private function askVersionUserWant(InputInterface $input, OutputInterface $output): ?int 39 | { 40 | /** @var QuestionHelper $helper */ 41 | $helper = $this->getHelper('question'); 42 | $question = new Question('Specify migration version(number). Skip step by pressing "Enter" to select latest available version. Enter 0 to revert all migrations: ', null); 43 | 44 | $answer = $helper->ask($input, $output, $question); 45 | 46 | if($answer === null) { 47 | return $answer; 48 | } 49 | 50 | if(!ctype_digit($answer)) { 51 | throw new \Exception('Incorrect input.'); 52 | } 53 | 54 | return (int)$answer; 55 | } 56 | } -------------------------------------------------------------------------------- /src/Commands/StartAppCommand.php: -------------------------------------------------------------------------------- 1 | setDescription('Start the application(Chat)'); 17 | } 18 | 19 | protected function execute(InputInterface $input, OutputInterface $output) 20 | { 21 | new WebsocketServer(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Config.php.sample: -------------------------------------------------------------------------------- 1 | PDO::ERRMODE_EXCEPTION, 10 | PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, 11 | PDO::ATTR_EMULATE_PREPARES => TRUE, 12 | ); 13 | 14 | const HOST = 'db'; 15 | const PORT = 3306; 16 | const USER = 'test'; 17 | const PASSWORD = 'test'; 18 | const DATABASE = 'ws-chat'; 19 | 20 | const SRCDIR = __DIR__; 21 | } -------------------------------------------------------------------------------- /src/Helper/DatabaseHelper.php: -------------------------------------------------------------------------------- 1 | exec('SELECT 1'); 56 | } catch (PDOException $e) { 57 | self::$pdo = self::initPdo(); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/Helper/PurifierHelper.php: -------------------------------------------------------------------------------- 1 | purify($stringToPurify, $config); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Helper/RequestLimiter.php: -------------------------------------------------------------------------------- 1 | userIds = new Channel(1024 * 64); 20 | } 21 | 22 | /** 23 | * Check if there are too many requests from user 24 | * and make a record of request from that user 25 | * 26 | * @param int $userId 27 | * @return bool 28 | */ 29 | public function checkIsRequestAllowed(int $userId) { 30 | $requestsCount = $this->getRequestsCountByUser($userId); 31 | $this->addRecord($userId); 32 | if ($requestsCount >= self::MAX_REQUESTS_BY_USER) return false; 33 | return true; 34 | } 35 | 36 | /** 37 | * @param int $userId 38 | * @return int 39 | */ 40 | private function getRequestsCountByUser(int $userId) { 41 | $channelRecordsCount = $this->userIds->stats()['queue_num']; 42 | $requestsCount = 0; 43 | 44 | for ($i = 0; $i < $channelRecordsCount; $i++) { 45 | $userIdFromChannel = $this->userIds->pop(); 46 | $this->userIds->push($userIdFromChannel); 47 | if ($userIdFromChannel === $userId) { 48 | $requestsCount++; 49 | } 50 | } 51 | 52 | return $requestsCount; 53 | } 54 | 55 | /** 56 | * @param int $userId 57 | */ 58 | private function addRecord(int $userId): void { 59 | $recordsCount = $this->userIds->stats()['queue_num']; 60 | 61 | if ($recordsCount >= self::MAX_SAVED_RECORDS_COUNT) { 62 | $this->userIds->pop(); 63 | } 64 | 65 | $this->userIds->push($userId); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Helper/SpamFilter.php: -------------------------------------------------------------------------------- 1 | errors[] = 'Empty message text'; 21 | $isCorrect = false; 22 | } 23 | return $isCorrect; 24 | } 25 | 26 | /** 27 | * @return string[] errors 28 | */ 29 | public function getErrors(): array { 30 | return $this->errors; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Message.php: -------------------------------------------------------------------------------- 1 | username = $username; 30 | $this->message = $message; 31 | $this->date_time = $date_time; 32 | } 33 | 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function getUsername(): string { 39 | return $this->username; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getMessage(): string { 46 | return $this->message; 47 | } 48 | 49 | /** 50 | * @return \DateTime 51 | */ 52 | public function getDateTime(): \DateTime { 53 | return $this->date_time; 54 | } 55 | } -------------------------------------------------------------------------------- /src/Migration/Version20190723174016730.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 21 | $this->createMigrationInfoTableIfNeeded(); 22 | } 23 | 24 | public function getCurrentSchemaVersion(): int 25 | { 26 | $versions = $this->pdo->query('SELECT current_version from migrations_info')->fetchAll(); 27 | if (empty($versions)) { 28 | throw new MigratorException('Something wrong with versions table(more than 1 version record founded)'); 29 | } 30 | return (int)end($versions)['current_version']; 31 | } 32 | 33 | public function updateSchemaVersion(int $version): void 34 | { 35 | $res = $this->pdo->exec('UPDATE migrations_info SET current_version = ' . $version); 36 | if ($res === false) { 37 | $this->pdo->exec('END TRANSACTION'); 38 | throw new MigratorException("Can`t save schema version:\n" . print_r($this->pdo->errorInfo(), true) . "\n"); 39 | } 40 | } 41 | 42 | private function createMigrationInfoTableIfNeeded(): void 43 | { 44 | if ($this->pdo->exec('CREATE TABLE IF NOT EXISTS migrations_info ("current_version" varchar(30))') === false) { 45 | throw new MigratorException("Can`t create migrations_info table:\n" . print_r($this->pdo->errorInfo(), true) . "\n"); 46 | } 47 | 48 | $PDOStatement = $this->pdo->query('SELECT current_version FROM migrations_info'); 49 | if ($PDOStatement === false) { 50 | throw new MigratorException("Can`t save schema version:\n" . print_r($this->pdo->errorInfo(), true) . "\n"); 51 | } 52 | $count = $PDOStatement->rowCount(); 53 | 54 | if ($count === 0) { 55 | if ($this->pdo->exec('INSERT INTO migrations_info VALUES(0)') === false) { 56 | throw new MigratorException("Can`t save schema version:\n" . print_r($this->pdo->errorInfo(), true) . "\n"); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/MigrationsComponent/Internal/MigrationExecutor.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 18 | } 19 | 20 | /** 21 | * @throws MigratorException 22 | * @throws \ReflectionException 23 | */ 24 | public function up(Migration $migration, DBCurrentSchemaVersionProvider $schemaVersionProvider): void 25 | { 26 | $res = $this->pdo->exec('START TRANSACTION'); 27 | if($res === false) { 28 | throw new MigratorException("Can`t start transaction:\n" . print_r($this->pdo->errorInfo(), true) . "\n"); 29 | } 30 | 31 | $res = $this->pdo->exec($migration->up()); 32 | if($res === false) { 33 | $this->pdo->exec('END TRANSACTION'); 34 | throw new MigratorException("Can`t aply migration:\n" . print_r($this->pdo->errorInfo(), true) . "\n"); 35 | } 36 | $schemaVersionProvider->updateSchemaVersion($migration->currentVersion()); 37 | 38 | $res = $this->pdo->exec('END TRANSACTION'); 39 | if($res === false) { 40 | throw new MigratorException("Can`t commit transaction:\n" . print_r($this->pdo->errorInfo(), true) . "\n"); 41 | } 42 | } 43 | 44 | /** 45 | * @throws MigratorException 46 | */ 47 | public function down(Migration $migration, DBCurrentSchemaVersionProvider $schemaVersionProvider): void 48 | { 49 | $res = $this->pdo->exec('START TRANSACTION'); 50 | if($res === false) { 51 | throw new MigratorException("Can`t start transaction:\n" . print_r($this->pdo->errorInfo(), true) . "\n"); 52 | } 53 | 54 | $res = $this->pdo->exec($migration->down()); 55 | if($res === false) { 56 | $this->pdo->exec('END TRANSACTION'); 57 | throw new MigratorException("Can`t aply migration:\n" . print_r($this->pdo->errorInfo(), true) . "\n"); 58 | } 59 | 60 | $schemaVersionProvider->updateSchemaVersion($migration->previousVersion()); 61 | 62 | $res = $this->pdo->exec('END TRANSACTION'); 63 | if($res === false) { 64 | throw new MigratorException("Can`t commit transaction:\n" . print_r($this->pdo->errorInfo(), true) . "\n"); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/MigrationsComponent/Internal/MigrationsLoader.php: -------------------------------------------------------------------------------- 1 | migrationsFolderPath = $migrationsFolderPath; 20 | } 21 | 22 | /** 23 | * @return Migration[] [version(int) => Migration class instance] 24 | * @throws \ReflectionException 25 | * @throws MigratorException 26 | */ 27 | public function loadAllMigrations(): array { 28 | $migrationFiles = []; 29 | $scandir = scandir($this->migrationsFolderPath); 30 | foreach (array_diff($scandir, ['.', '..']) as $filename) { 31 | /** 32 | * @psalm-var class-string 33 | * @var Migration $fullClassName 34 | */ 35 | $fullClassName = $this->getFullClassName($filename); 36 | $migrationFiles[$fullClassName::currentVersion()] = $fullClassName; 37 | } 38 | 39 | $res = ksort($migrationFiles); 40 | if($res === false) { 41 | throw new MigratorException('Can`t use ksort()'); 42 | } 43 | 44 | $migrations = []; 45 | $prevVersion = 0; 46 | foreach ($migrationFiles as $version => $fullClassName) { 47 | $migrations[$version] = new $fullClassName($prevVersion); 48 | $prevVersion = $version; 49 | } 50 | return $migrations; 51 | } 52 | 53 | /** 54 | * @throws MigratorException 55 | */ 56 | private function getFullClassName(string $migrationFileName): string 57 | { 58 | $migrationsNamespace = 'App\Migration'; 59 | preg_match('/^(Version\d+)\.php$/', $migrationFileName, $matches); 60 | if(!isset($matches[1])) { 61 | throw new MigratorException('Incorrect migration filename'); 62 | } 63 | $className = $matches[1]; 64 | return $migrationsNamespace . '\\' . $className; 65 | } 66 | 67 | private function isMigrationFile(string $filename): bool 68 | { 69 | return (bool)preg_match('/^Version\d+\.php$/', $filename); 70 | } 71 | } -------------------------------------------------------------------------------- /src/MigrationsComponent/Internal/MigratorException.php: -------------------------------------------------------------------------------- 1 | loader = $loader; 25 | $this->schemaVersionProvider = $schemaVersionProvider; 26 | $this->migrationExecutor = $executor; 27 | } 28 | 29 | /** 30 | * @return string One of predefined constants text 31 | * @throws \ReflectionException 32 | * @throws MigratorException 33 | */ 34 | public function migrateToVersion(?int $requiredVersion): string 35 | { 36 | $migrations = $this->loader->loadAllMigrations(); 37 | $currentVersion = $this->schemaVersionProvider->getCurrentSchemaVersion(); 38 | $latestVersion = (int)max(array_keys($migrations)); 39 | $requiredVersion = $requiredVersion ?? $latestVersion; 40 | 41 | if ($requiredVersion === $currentVersion) { 42 | return MigratorResultText::REQUIRED_AND_CURRENT_VERSION_ARE_SAME; 43 | } 44 | 45 | if(!array_key_exists($requiredVersion, $migrations) && $requiredVersion !== 0) { 46 | throw new MigratorException('Incorrect input. Specified version not found'); 47 | } 48 | 49 | 50 | if ($requiredVersion > $currentVersion) { 51 | $migrationsToUp = array_filter($migrations, function (int $version) use ($currentVersion, $requiredVersion) { 52 | return ($version > $currentVersion && $version <= $requiredVersion); 53 | }, ARRAY_FILTER_USE_KEY); 54 | 55 | $res = ksort($migrationsToUp); 56 | if ($res === false) { 57 | throw new MigratorException('Can`t use ksort()'); 58 | } 59 | 60 | foreach ($migrationsToUp as $version => $migrationToUp) { 61 | $this->migrationExecutor->up($migrationToUp, $this->schemaVersionProvider); 62 | } 63 | return MigratorResultText::SUCCESS; 64 | } 65 | 66 | if ($requiredVersion < $currentVersion) { 67 | $migrationsToRevert = array_filter($migrations, function (int $version) use ($currentVersion, $requiredVersion) { 68 | return ($version <= $currentVersion && $version > $requiredVersion); 69 | }, ARRAY_FILTER_USE_KEY); 70 | 71 | $res = krsort($migrationsToRevert); 72 | if ($res === false) { 73 | throw new MigratorException('Can`t use krsort()'); 74 | } 75 | 76 | foreach ($migrationsToRevert as $migrationToRevert) { 77 | $this->migrationExecutor->down($migrationToRevert, $this->schemaVersionProvider); 78 | } 79 | return MigratorResultText::SUCCESS; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/MigrationsComponent/Migration.php: -------------------------------------------------------------------------------- 1 | previousVersion = $previousVersion; 17 | } 18 | 19 | /** 20 | * @throws MigratorException 21 | * @throws \ReflectionException 22 | */ 23 | public static function currentVersion(): int { 24 | $reflection = new \ReflectionClass(get_called_class()); 25 | $className = $reflection->getShortName(); 26 | preg_match('/^Version(\d+)$/', $className, $matches); 27 | if(!isset($matches[1])) { 28 | throw new MigratorException('Incorrect migration filename. Can`t get version'); 29 | } 30 | return (int)$matches[1]; 31 | } 32 | 33 | public function previousVersion(): int { 34 | return $this->previousVersion; 35 | } 36 | 37 | /** 38 | * @return string SQL 39 | */ 40 | abstract public function up(): string ; 41 | 42 | /** 43 | * @return string SQL 44 | */ 45 | abstract public function down(): string ; 46 | } -------------------------------------------------------------------------------- /src/MigrationsComponent/Migrator.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 38 | $this->schemaVersionProvider = new DBCurrentSchemaVersionProvider($this->pdo); 39 | $this->migrationsLoader = new MigrationsLoader($migrationsFolderPath); 40 | } 41 | 42 | /** 43 | * @param int $requiredVersion 44 | * Pass 0 version to drop all migrations, pass null to apply all new migrations 45 | * @throws \ReflectionException 46 | * @throws MigratorException 47 | */ 48 | public function updateSchema(?int $requiredVersion): string { 49 | $this->migrationsLoader->loadAllMigrations(); 50 | $schemaUpdater = new SchemaUpdater($this->migrationsLoader, $this->schemaVersionProvider, new MigrationExecutor($this->pdo)); 51 | return $schemaUpdater->migrateToVersion($requiredVersion); 52 | } 53 | } -------------------------------------------------------------------------------- /src/Repository/MessagesRepository.php: -------------------------------------------------------------------------------- 1 | pdo = DatabaseHelper::pdoInstance(); 21 | } 22 | 23 | /** 24 | * @return Message[] 25 | * @throws \Exception 26 | */ 27 | public function getAll():array { 28 | $stmt = $this->pdo->query('SELECT * FROM messages ORDER BY date_time DESC LIMIT 100'); 29 | $messages = []; 30 | foreach ($stmt->fetchAll() as $row) { 31 | $messages[] = new Message($row['username'], $row['message'], new \DateTime($row['date_time'])); 32 | } 33 | return $messages; 34 | } 35 | 36 | /** 37 | * @param Message $message 38 | */ 39 | public function save(Message $message): void { 40 | $stmt = $this->pdo->prepare("INSERT INTO messages (username, message) VALUES (:username, :message)"); 41 | $stmt->execute(array('username' => $message->getUsername(), 'message' => $message->getMessage())); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Repository/UsersRepository.php: -------------------------------------------------------------------------------- 1 | reCreateUsersTable(); 18 | } 19 | 20 | /** 21 | * @param int $id 22 | * @return User|false 23 | */ 24 | public function get(int $id) { 25 | $userRow = $this->users_table->get($id); 26 | if ($userRow !== false) { 27 | return new User($id, $userRow['username']); 28 | } 29 | return false; 30 | } 31 | 32 | /** 33 | * Get all online users 34 | * @param int[] $ids 35 | * @return User[] 36 | */ 37 | public function getByIds(array $ids) { 38 | $users = []; 39 | foreach ($ids as $id) { 40 | $row = $this->users_table->get($id); 41 | if ($row !== false) { 42 | $users[] = new User($id, $row['username']); 43 | } 44 | } 45 | return $users; 46 | } 47 | 48 | /** 49 | * @param int $userId 50 | */ 51 | public function delete(int $userId): void { 52 | $this->users_table->del($userId); 53 | } 54 | 55 | /** 56 | * Save user to table in memory; 57 | * @param User $user 58 | */ 59 | public function save(User $user): void { 60 | $result = $this->users_table->set($user->getId(), ['username' => $user->getUsername()]); 61 | if ($result === false) { 62 | $this->reCreateUsersTable(); 63 | $this->users_table->set($user->getId(), ['username' => $user->getUsername()]); 64 | } 65 | } 66 | 67 | public function reCreateUsersTable(): void { 68 | if (isset($this->users_table)) { 69 | $this->users_table->destroy(); 70 | } 71 | $this->users_table = new swoole_table(131072); 72 | $this->users_table->column('username', swoole_table::TYPE_STRING, 100); 73 | $this->users_table->create(); 74 | } 75 | } -------------------------------------------------------------------------------- /src/Request/LoginRequest.php: -------------------------------------------------------------------------------- 1 | userId = $userId; 13 | $this->username = $username; 14 | } 15 | 16 | /** 17 | * @return int 18 | */ 19 | public function getUserId(): int { 20 | return $this->userId; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getUsername(): string { 27 | return $this->username; 28 | } 29 | } -------------------------------------------------------------------------------- /src/Request/MessageRequest.php: -------------------------------------------------------------------------------- 1 | userId = $userId; 13 | $this->message = $message; 14 | } 15 | 16 | /** 17 | * @return int 18 | */ 19 | public function getUserId(): int { 20 | return $this->userId; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getMessage(): string { 27 | return $this->message; 28 | } 29 | } -------------------------------------------------------------------------------- /src/Response/ErrorJsonReponse.php: -------------------------------------------------------------------------------- 1 | message = $message; 12 | } 13 | 14 | protected function getType(): string { 15 | return 'error'; 16 | } 17 | 18 | protected function getBody() { 19 | return ['message' => $this->message]; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Response/JsonReponse.php: -------------------------------------------------------------------------------- 1 | $this->getType(), 'body' => $this->getBody()]); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Response/LoginJsonReponse.php: -------------------------------------------------------------------------------- 1 | result = $result; 16 | $this->username = $username; 17 | $this->message = $message; 18 | } 19 | 20 | protected function getType(): string { 21 | return 'login'; 22 | } 23 | 24 | protected function getBody() { 25 | if ($this->message != null) { 26 | $body = ['result' => $this->result, 'username' => $this->username, 'message' => $this->message]; 27 | } else { 28 | $body = ['result' => $this->result, 'username' => $this->username]; 29 | } 30 | return $body; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Response/MessagesJsonReponse.php: -------------------------------------------------------------------------------- 1 | $this->messages]; 22 | } 23 | 24 | /** 25 | * @param Message $message 26 | * @return MessagesJsonReponse 27 | */ 28 | public function addMessage(Message $message) { 29 | $purifiedMessage = PurifierHelper::purify($message->getMessage()); 30 | $timestamp = $message->getDateTime()->getTimestamp(); 31 | $this->messages[] = [ 32 | 'username' => $message->getUsername(), 33 | 'message' => $purifiedMessage, 34 | 'dateTime' => $timestamp 35 | ]; 36 | return $this; 37 | } 38 | } -------------------------------------------------------------------------------- /src/Response/UsersJsonReponse.php: -------------------------------------------------------------------------------- 1 | action = $action; 26 | } 27 | 28 | protected function getType(): string { 29 | return 'users'; 30 | } 31 | 32 | protected function getBody(): array { 33 | return ['action' => $this->action, 'users' => $this->users]; 34 | } 35 | 36 | /** 37 | * @param User $user 38 | * @return UsersJsonReponse 39 | */ 40 | public function addUser(User $user) { 41 | $this->users[] = [ 42 | 'id' => $user->getId(), 43 | 'name' => $user->getUsername() 44 | ]; 45 | return $this; 46 | } 47 | } -------------------------------------------------------------------------------- /src/User.php: -------------------------------------------------------------------------------- 1 | id = $id; 23 | $this->username = $username; 24 | } 25 | 26 | /** 27 | * @return int 28 | */ 29 | public function getId(): int { 30 | return $this->id; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getUsername(): string { 37 | return $this->username; 38 | } 39 | } -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix'); 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Mix Asset Management 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Mix provides a clean, fluent API for defining some Webpack build steps 9 | | for your Laravel application. By default, we are compiling the Sass 10 | | file for your application, as well as bundling up your JS files. 11 | | 12 | */ 13 | 14 | mix.js('resources/js/app.js', './public/js/app.js').sass('resources/css/app.scss', './public/css/app.css'); 15 | 16 | // Full API 17 | // mix.js(src, output); 18 | // mix.react(src, output); <-- Identical to mix.js(), but registers React Babel compilation. 19 | // mix.preact(src, output); <-- Identical to mix.js(), but registers Preact compilation. 20 | // mix.coffee(src, output); <-- Identical to mix.js(), but registers CoffeeScript compilation. 21 | // mix.ts(src, output); <-- TypeScript support. Requires tsconfig.json to exist in the same folder as webpack.mix.js 22 | // mix.extract(vendorLibs); 23 | // mix.sass(src, output); 24 | // mix.standaloneSass('src', output); <-- Faster, but isolated from Webpack. 25 | // mix.fastSass('src', output); <-- Alias for mix.standaloneSass(). 26 | // mix.less(src, output); 27 | // mix.stylus(src, output); 28 | // mix.postCss(src, output, [require('postcss-some-plugin')()]); 29 | // mix.browserSync('my-site.test'); 30 | // mix.combine(files, destination); 31 | // mix.babel(files, destination); <-- Identical to mix.combine(), but also includes Babel compilation. 32 | // mix.copy(from, to); 33 | // mix.copyDirectory(fromDir, toDir); 34 | // mix.minify(file); 35 | // mix.sourceMaps(); // Enable sourcemaps 36 | // mix.version(); // Enable versioning. 37 | // mix.disableNotifications(); 38 | // mix.setPublicPath('path/to/public'); 39 | // mix.setResourceRoot('prefix/for/resource/locators'); 40 | // mix.autoload({}); <-- Will be passed to Webpack's ProvidePlugin. 41 | // mix.webpackConfig({}); <-- Override webpack.config.js, without editing the file directly. 42 | // mix.babelConfig({}); <-- Merge extra Babel configuration (plugins, etc.) with Mix's default. 43 | // mix.then(function () {}) <-- Will be triggered each time Webpack finishes building. 44 | // mix.extend(name, handler) <-- Extend Mix's API with your own components. 45 | // mix.options({ 46 | // extractVueStyles: false, // Extract .vue component styling to file, rather than inline. 47 | // globalVueStyles: file, // Variables file to be imported in every component. 48 | // processCssUrls: true, // Process/optimize relative stylesheet url()'s. Set to false, if you don't want them touched. 49 | // purifyCss: false, // Remove unused CSS selectors. 50 | // uglify: {}, // Uglify-specific options. https://webpack.github.io/docs/list-of-plugins.html#uglifyjsplugin 51 | // postCss: [] // Post-CSS options: https://github.com/postcss/postcss/blob/master/docs/plugins.md 52 | // }); 53 | --------------------------------------------------------------------------------