Repository: phoenixframework/phoenix
Branch: main
Commit: eca672ec4bdc
Files: 454
Total size: 3.1 MB
Directory structure:
gitextract_fy0ivb69/
├── .formatter.exs
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── config.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── assets.yml
│ ├── ci.yml
│ └── npm-publish.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── RELEASE.md
├── SECURITY.md
├── assets/
│ ├── js/
│ │ └── phoenix/
│ │ ├── ajax.js
│ │ ├── channel.js
│ │ ├── constants.js
│ │ ├── index.js
│ │ ├── longpoll.js
│ │ ├── presence.js
│ │ ├── push.js
│ │ ├── serializer.js
│ │ ├── socket.js
│ │ ├── timer.js
│ │ └── utils.js
│ └── test/
│ ├── channel_test.js
│ ├── longpoll_test.js
│ ├── presence_test.js
│ ├── serializer.js
│ ├── serializer_test.js
│ ├── socket_http_test.js
│ └── socket_test.js
├── babel.config.json
├── config/
│ └── config.exs
├── eslint.config.mjs
├── guides/
│ ├── asset_management.md
│ ├── authn_authz/
│ │ ├── api_authentication.md
│ │ ├── authn_authz.md
│ │ ├── mix_phx_gen_auth.md
│ │ └── scopes.md
│ ├── cheatsheets/
│ │ └── router.cheatmd
│ ├── components.md
│ ├── controllers.md
│ ├── data_modelling/
│ │ ├── contexts.md
│ │ ├── cross_context_boundaries.md
│ │ ├── faq.md
│ │ ├── in_context_relationships.md
│ │ ├── more_examples.md
│ │ └── your_first_context.md
│ ├── deployment/
│ │ ├── deployment.md
│ │ ├── fly.md
│ │ ├── gigalixir.md
│ │ ├── heroku.md
│ │ └── releases.md
│ ├── directory_structure.md
│ ├── ecto.md
│ ├── howto/
│ │ ├── custom_error_pages.md
│ │ ├── file_uploads.md
│ │ ├── swapping_databases.md
│ │ ├── using_ssl.md
│ │ └── writing_a_channels_client.md
│ ├── introduction/
│ │ ├── community.md
│ │ ├── installation.md
│ │ ├── overview.md
│ │ ├── packages_glossary.md
│ │ └── up_and_running.md
│ ├── json_and_apis.md
│ ├── live_view.md
│ ├── plug.md
│ ├── real_time/
│ │ ├── channels.md
│ │ └── presence.md
│ ├── request_lifecycle.md
│ ├── routing.md
│ ├── security.md
│ ├── telemetry.md
│ └── testing/
│ ├── testing.md
│ ├── testing_channels.md
│ ├── testing_contexts.md
│ └── testing_controllers.md
├── installer/
│ ├── .gitignore
│ ├── README.md
│ ├── lib/
│ │ ├── mix/
│ │ │ └── tasks/
│ │ │ ├── local.phx.ex
│ │ │ ├── phx.new.ecto.ex
│ │ │ ├── phx.new.ex
│ │ │ └── phx.new.web.ex
│ │ └── phx_new/
│ │ ├── ecto.ex
│ │ ├── generator.ex
│ │ ├── mailer.ex
│ │ ├── project.ex
│ │ ├── single.ex
│ │ ├── umbrella.ex
│ │ └── web.ex
│ ├── mix.exs
│ ├── recreate_default_css.exs
│ ├── templates/
│ │ ├── phx_assets/
│ │ │ ├── app.css.eex
│ │ │ ├── app.js.eex
│ │ │ ├── daisyui-theme.js.eex
│ │ │ ├── daisyui.js.eex
│ │ │ ├── heroicons.js.eex
│ │ │ ├── logo.svg.eex
│ │ │ ├── topbar.js.eex
│ │ │ └── tsconfig.json.eex
│ │ ├── phx_ecto/
│ │ │ ├── data_case.ex.eex
│ │ │ ├── formatter.exs.eex
│ │ │ ├── repo.ex.eex
│ │ │ └── seeds.exs.eex
│ │ ├── phx_gettext/
│ │ │ ├── en/
│ │ │ │ └── LC_MESSAGES/
│ │ │ │ └── errors.po.eex
│ │ │ ├── errors.pot.eex
│ │ │ └── gettext.ex.eex
│ │ ├── phx_mailer/
│ │ │ └── lib/
│ │ │ └── app_name/
│ │ │ └── mailer.ex.eex
│ │ ├── phx_single/
│ │ │ ├── README.md.eex
│ │ │ ├── config/
│ │ │ │ ├── config.exs.eex
│ │ │ │ ├── dev.exs.eex
│ │ │ │ ├── prod.exs.eex
│ │ │ │ ├── runtime.exs.eex
│ │ │ │ └── test.exs.eex
│ │ │ ├── formatter.exs.eex
│ │ │ ├── gitignore.eex
│ │ │ ├── lib/
│ │ │ │ ├── app_name/
│ │ │ │ │ └── application.ex.eex
│ │ │ │ ├── app_name.ex.eex
│ │ │ │ └── app_name_web.ex.eex
│ │ │ ├── mix.exs.eex
│ │ │ └── test/
│ │ │ └── test_helper.exs.eex
│ │ ├── phx_static/
│ │ │ ├── app.css
│ │ │ ├── app.js
│ │ │ ├── default.css
│ │ │ └── robots.txt
│ │ ├── phx_test/
│ │ │ ├── controllers/
│ │ │ │ ├── error_html_test.exs.eex
│ │ │ │ ├── error_json_test.exs.eex
│ │ │ │ └── page_controller_test.exs.eex
│ │ │ └── support/
│ │ │ └── conn_case.ex.eex
│ │ ├── phx_umbrella/
│ │ │ ├── README.md.eex
│ │ │ ├── apps/
│ │ │ │ ├── app_name/
│ │ │ │ │ ├── README.md.eex
│ │ │ │ │ ├── config/
│ │ │ │ │ │ └── config.exs.eex
│ │ │ │ │ ├── formatter.exs.eex
│ │ │ │ │ ├── gitignore.eex
│ │ │ │ │ ├── lib/
│ │ │ │ │ │ ├── app_name/
│ │ │ │ │ │ │ └── application.ex.eex
│ │ │ │ │ │ └── app_name.ex.eex
│ │ │ │ │ ├── mix.exs.eex
│ │ │ │ │ └── test/
│ │ │ │ │ └── test_helper.exs.eex
│ │ │ │ └── app_name_web/
│ │ │ │ ├── README.md.eex
│ │ │ │ ├── config/
│ │ │ │ │ ├── config.exs.eex
│ │ │ │ │ ├── dev.exs.eex
│ │ │ │ │ ├── prod.exs.eex
│ │ │ │ │ ├── runtime.exs.eex
│ │ │ │ │ └── test.exs.eex
│ │ │ │ ├── formatter.exs.eex
│ │ │ │ ├── gitignore.eex
│ │ │ │ ├── lib/
│ │ │ │ │ ├── app_name/
│ │ │ │ │ │ └── application.ex.eex
│ │ │ │ │ └── app_name.ex.eex
│ │ │ │ ├── mix.exs.eex
│ │ │ │ └── test/
│ │ │ │ └── test_helper.exs.eex
│ │ │ ├── config/
│ │ │ │ ├── config.exs.eex
│ │ │ │ ├── dev.exs.eex
│ │ │ │ ├── extra_config.exs.eex
│ │ │ │ ├── prod.exs.eex
│ │ │ │ ├── runtime.exs.eex
│ │ │ │ └── test.exs.eex
│ │ │ ├── formatter.exs.eex
│ │ │ ├── gitignore.eex
│ │ │ └── mix.exs.eex
│ │ ├── phx_web/
│ │ │ ├── components/
│ │ │ │ ├── core_components.ex.eex
│ │ │ │ ├── layouts/
│ │ │ │ │ └── root.html.heex.eex
│ │ │ │ └── layouts.ex.eex
│ │ │ ├── controllers/
│ │ │ │ ├── error_html.ex.eex
│ │ │ │ ├── error_json.ex.eex
│ │ │ │ ├── page_controller.ex.eex
│ │ │ │ ├── page_html/
│ │ │ │ │ └── home.html.heex.eex
│ │ │ │ └── page_html.ex.eex
│ │ │ ├── endpoint.ex.eex
│ │ │ ├── router.ex.eex
│ │ │ └── telemetry.ex.eex
│ │ └── usage-rules/
│ │ ├── assets.md
│ │ ├── phoenix.md
│ │ └── project.md
│ └── test/
│ ├── mix_helper.exs
│ ├── phx_new_ecto_test.exs
│ ├── phx_new_test.exs
│ ├── phx_new_umbrella_test.exs
│ ├── phx_new_web_test.exs
│ └── test_helper.exs
├── integration_test/
│ ├── README.md
│ ├── config/
│ │ └── config.exs
│ ├── docker-compose.yml
│ ├── docker.sh
│ ├── mix.exs
│ ├── test/
│ │ ├── code_generation/
│ │ │ ├── app_with_defaults_test.exs
│ │ │ ├── app_with_mssql_adapter_test.exs
│ │ │ ├── app_with_mysql_adapter_test.exs
│ │ │ ├── app_with_no_options_test.exs
│ │ │ ├── app_with_scopes_test.exs
│ │ │ ├── app_with_sqlite3_adapter_test.exs
│ │ │ └── umbrella_app_with_defaults_test.exs
│ │ ├── support/
│ │ │ └── code_generator_case.ex
│ │ └── test_helper.exs
│ └── test.sh
├── jest.config.js
├── lib/
│ ├── mix/
│ │ ├── phoenix/
│ │ │ ├── context.ex
│ │ │ ├── schema.ex
│ │ │ └── scope.ex
│ │ ├── phoenix.ex
│ │ └── tasks/
│ │ ├── compile.phoenix.ex
│ │ ├── phx.digest.clean.ex
│ │ ├── phx.digest.ex
│ │ ├── phx.ex
│ │ ├── phx.gen.auth/
│ │ │ ├── hashing_library.ex
│ │ │ ├── injector.ex
│ │ │ └── migration.ex
│ │ ├── phx.gen.auth.ex
│ │ ├── phx.gen.cert.ex
│ │ ├── phx.gen.channel.ex
│ │ ├── phx.gen.context.ex
│ │ ├── phx.gen.embedded.ex
│ │ ├── phx.gen.ex
│ │ ├── phx.gen.html.ex
│ │ ├── phx.gen.json.ex
│ │ ├── phx.gen.live.ex
│ │ ├── phx.gen.notifier.ex
│ │ ├── phx.gen.presence.ex
│ │ ├── phx.gen.release.ex
│ │ ├── phx.gen.schema.ex
│ │ ├── phx.gen.secret.ex
│ │ ├── phx.gen.socket.ex
│ │ ├── phx.routes.ex
│ │ └── phx.server.ex
│ ├── phoenix/
│ │ ├── channel/
│ │ │ └── server.ex
│ │ ├── channel.ex
│ │ ├── code_reloader/
│ │ │ ├── mix_listener.ex
│ │ │ ├── proxy.ex
│ │ │ └── server.ex
│ │ ├── code_reloader.ex
│ │ ├── config.ex
│ │ ├── controller/
│ │ │ └── pipeline.ex
│ │ ├── controller.ex
│ │ ├── debug.ex
│ │ ├── digester/
│ │ │ ├── compressor.ex
│ │ │ └── gzip.ex
│ │ ├── digester.ex
│ │ ├── endpoint/
│ │ │ ├── cowboy2_adapter.ex
│ │ │ ├── render_errors.ex
│ │ │ ├── supervisor.ex
│ │ │ ├── sync_code_reload_plug.ex
│ │ │ └── watcher.ex
│ │ ├── endpoint.ex
│ │ ├── exceptions.ex
│ │ ├── flash.ex
│ │ ├── logger.ex
│ │ ├── naming.ex
│ │ ├── param.ex
│ │ ├── presence.ex
│ │ ├── router/
│ │ │ ├── console_formatter.ex
│ │ │ ├── helpers.ex
│ │ │ ├── resource.ex
│ │ │ ├── route.ex
│ │ │ └── scope.ex
│ │ ├── router.ex
│ │ ├── socket/
│ │ │ ├── message.ex
│ │ │ ├── pool_supervisor.ex
│ │ │ ├── serializer.ex
│ │ │ ├── serializers/
│ │ │ │ ├── v1_json_serializer.ex
│ │ │ │ └── v2_json_serializer.ex
│ │ │ └── transport.ex
│ │ ├── socket.ex
│ │ ├── test/
│ │ │ ├── channel_test.ex
│ │ │ └── conn_test.ex
│ │ ├── token.ex
│ │ ├── transports/
│ │ │ ├── long_poll.ex
│ │ │ ├── long_poll_server.ex
│ │ │ └── websocket.ex
│ │ └── verified_routes.ex
│ └── phoenix.ex
├── mix.exs
├── package.json
├── priv/
│ ├── static/
│ │ ├── phoenix.cjs.js
│ │ ├── phoenix.js
│ │ └── phoenix.mjs
│ └── templates/
│ ├── phx.gen.auth/
│ │ ├── AGENTS.md.eex
│ │ ├── auth.ex.eex
│ │ ├── auth_test.exs.eex
│ │ ├── confirmation_live.ex.eex
│ │ ├── confirmation_live_test.exs.eex
│ │ ├── conn_case.exs.eex
│ │ ├── context_fixtures_functions.ex.eex
│ │ ├── context_functions.ex.eex
│ │ ├── login_live.ex.eex
│ │ ├── login_live_test.exs.eex
│ │ ├── migration.ex.eex
│ │ ├── notifier.ex.eex
│ │ ├── registration_controller.ex.eex
│ │ ├── registration_controller_test.exs.eex
│ │ ├── registration_html.ex.eex
│ │ ├── registration_live.ex.eex
│ │ ├── registration_live_test.exs.eex
│ │ ├── registration_new.html.heex.eex
│ │ ├── routes.ex.eex
│ │ ├── schema.ex.eex
│ │ ├── schema_token.ex.eex
│ │ ├── scope.ex.eex
│ │ ├── session_confirm.html.heex.eex
│ │ ├── session_controller.ex.eex
│ │ ├── session_controller_test.exs.eex
│ │ ├── session_html.ex.eex
│ │ ├── session_new.html.heex.eex
│ │ ├── settings_controller.ex.eex
│ │ ├── settings_controller_test.exs.eex
│ │ ├── settings_edit.html.heex.eex
│ │ ├── settings_html.ex.eex
│ │ ├── settings_live.ex.eex
│ │ ├── settings_live_test.exs.eex
│ │ └── test_cases.exs.eex
│ ├── phx.gen.channel/
│ │ ├── channel.ex.eex
│ │ ├── channel_case.ex.eex
│ │ └── channel_test.exs.eex
│ ├── phx.gen.context/
│ │ ├── access_no_schema.ex.eex
│ │ ├── access_no_schema_scope.ex.eex
│ │ ├── context.ex.eex
│ │ ├── context_test.exs.eex
│ │ ├── fixtures.ex.eex
│ │ ├── fixtures_module.ex.eex
│ │ ├── schema_access.ex.eex
│ │ ├── schema_access_scope.ex.eex
│ │ ├── test_cases.exs.eex
│ │ └── test_cases_scope.exs.eex
│ ├── phx.gen.embedded/
│ │ └── embedded_schema.ex.eex
│ ├── phx.gen.html/
│ │ ├── controller.ex.eex
│ │ ├── controller_test.exs.eex
│ │ ├── edit.html.heex.eex
│ │ ├── html.ex.eex
│ │ ├── index.html.heex.eex
│ │ ├── new.html.heex.eex
│ │ ├── resource_form.html.heex.eex
│ │ └── show.html.heex.eex
│ ├── phx.gen.json/
│ │ ├── changeset_json.ex.eex
│ │ ├── controller.ex.eex
│ │ ├── controller_test.exs.eex
│ │ ├── fallback_controller.ex.eex
│ │ └── json.ex.eex
│ ├── phx.gen.live/
│ │ ├── form.ex.eex
│ │ ├── index.ex.eex
│ │ ├── live_test.exs.eex
│ │ └── show.ex.eex
│ ├── phx.gen.notifier/
│ │ ├── notifier.ex.eex
│ │ └── notifier_test.exs.eex
│ ├── phx.gen.presence/
│ │ └── presence.ex.eex
│ ├── phx.gen.release/
│ │ ├── Dockerfile.eex
│ │ ├── dockerignore.eex
│ │ ├── rel/
│ │ │ ├── migrate.bat.eex
│ │ │ ├── migrate.sh.eex
│ │ │ ├── server.bat.eex
│ │ │ └── server.sh.eex
│ │ └── release.ex.eex
│ ├── phx.gen.schema/
│ │ ├── migration.exs.eex
│ │ └── schema.ex.eex
│ └── phx.gen.socket/
│ ├── socket.ex.eex
│ └── socket.js.eex
├── test/
│ ├── fixtures/
│ │ ├── digest/
│ │ │ ├── cleaner/
│ │ │ │ ├── cache_manifest.json
│ │ │ │ └── latest_not_most_recent_cache_manifest.json
│ │ │ ├── compile/
│ │ │ │ ├── cache_manifest.json
│ │ │ │ └── cache_manifest_upgrade.json
│ │ │ └── priv/
│ │ │ ├── output/
│ │ │ │ ├── foo-288ea8c7954498e65663c817382eeac4.css
│ │ │ │ └── foo-d978852bea6530fcd197b5445ed008fd.css
│ │ │ └── static/
│ │ │ ├── app.js
│ │ │ ├── css/
│ │ │ │ └── app.css
│ │ │ ├── foo.css
│ │ │ ├── manifest.json
│ │ │ └── precompressed.js.br
│ │ ├── hello.txt
│ │ ├── ssl/
│ │ │ ├── cert.pem
│ │ │ └── key.pem
│ │ ├── templates/
│ │ │ ├── custom.foo
│ │ │ ├── layout/
│ │ │ │ ├── app.html.eex
│ │ │ │ └── root.html.eex
│ │ │ ├── no_trim.text.eex
│ │ │ ├── path.html.eex
│ │ │ ├── safe.html.eex
│ │ │ ├── show.html.eex
│ │ │ ├── trim.html.eex
│ │ │ └── user/
│ │ │ ├── index.html.eex
│ │ │ ├── profiles/
│ │ │ │ └── admin.html.eex
│ │ │ ├── render_template.html.eex
│ │ │ └── show.json.exs
│ │ └── views.exs
│ ├── mix/
│ │ ├── phoenix_test.exs
│ │ └── tasks/
│ │ ├── phx.digest.clean_test.exs
│ │ ├── phx.digest_test.exs
│ │ ├── phx.gen.auth/
│ │ │ └── injector_test.exs
│ │ ├── phx.gen.auth_test.exs
│ │ ├── phx.gen.cert_test.exs
│ │ ├── phx.gen.channel_test.exs
│ │ ├── phx.gen.context_test.exs
│ │ ├── phx.gen.embedded_test.exs
│ │ ├── phx.gen.html_test.exs
│ │ ├── phx.gen.json_test.exs
│ │ ├── phx.gen.live_test.exs
│ │ ├── phx.gen.notifier_test.exs
│ │ ├── phx.gen.presence_test.exs
│ │ ├── phx.gen.release_test.exs
│ │ ├── phx.gen.schema_test.exs
│ │ ├── phx.gen.secret_test.exs
│ │ ├── phx.gen.socket_test.exs
│ │ ├── phx.routes_test.exs
│ │ └── phx_test.exs
│ ├── phoenix/
│ │ ├── channel_test.exs
│ │ ├── code_reloader_test.exs
│ │ ├── config_test.exs
│ │ ├── controller/
│ │ │ ├── controller_test.exs
│ │ │ ├── flash_test.exs
│ │ │ ├── pipeline_test.exs
│ │ │ └── render_test.exs
│ │ ├── debug_test.exs
│ │ ├── digester/
│ │ │ └── gzip_test.exs
│ │ ├── digester_test.exs
│ │ ├── endpoint/
│ │ │ ├── endpoint_test.exs
│ │ │ ├── render_errors_test.exs
│ │ │ ├── supervisor_test.exs
│ │ │ └── watcher_test.exs
│ │ ├── integration/
│ │ │ ├── endpoint_test.exs
│ │ │ ├── long_poll_channels_test.exs
│ │ │ ├── long_poll_socket_test.exs
│ │ │ ├── websocket_channels_test.exs
│ │ │ └── websocket_socket_test.exs
│ │ ├── logger_test.exs
│ │ ├── naming_test.exs
│ │ ├── param_test.exs
│ │ ├── presence_test.exs
│ │ ├── router/
│ │ │ ├── console_formatter_test.exs
│ │ │ ├── forward_test.exs
│ │ │ ├── helpers_test.exs
│ │ │ ├── pipeline_test.exs
│ │ │ ├── resource_test.exs
│ │ │ ├── resources_test.exs
│ │ │ ├── route_test.exs
│ │ │ ├── routing_test.exs
│ │ │ └── scope_test.exs
│ │ ├── socket/
│ │ │ ├── message_test.exs
│ │ │ ├── socket_test.exs
│ │ │ ├── transport_test.exs
│ │ │ ├── v1_json_serializer_test.exs
│ │ │ └── v2_json_serializer_test.exs
│ │ ├── test/
│ │ │ ├── channel_test.exs
│ │ │ └── conn_test.exs
│ │ ├── token_test.exs
│ │ └── verified_routes_test.exs
│ ├── support/
│ │ ├── endpoint_helper.exs
│ │ ├── http_client.exs
│ │ ├── router_helper.exs
│ │ └── websocket_client.exs
│ └── test_helper.exs
└── usage-rules/
├── ecto.md
├── elixir.md
├── html.md
├── liveview.md
└── phoenix.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .formatter.exs
================================================
locals_without_parens = [
# Phoenix.Channel
intercept: 1,
# Phoenix.Router
connect: 3,
connect: 4,
delete: 3,
delete: 4,
forward: 2,
forward: 3,
forward: 4,
get: 3,
get: 4,
head: 3,
head: 4,
match: 4,
match: 5,
options: 3,
options: 4,
patch: 3,
patch: 4,
pipeline: 2,
pipe_through: 1,
post: 3,
post: 4,
put: 3,
put: 4,
resources: 2,
resources: 3,
resources: 4,
trace: 4,
# Phoenix.Controller
action_fallback: 1,
# Phoenix.Endpoint
plug: 1,
plug: 2,
socket: 2,
socket: 3,
# Phoenix.Socket
channel: 2,
channel: 3,
# Phoenix.ChannelTest
assert_broadcast: 2,
assert_broadcast: 3,
assert_push: 2,
assert_push: 3,
assert_reply: 2,
assert_reply: 3,
assert_reply: 4,
refute_broadcast: 2,
refute_broadcast: 3,
refute_push: 2,
refute_push: 3,
refute_reply: 2,
refute_reply: 3,
refute_reply: 4,
# Phoenix.ConnTest
assert_error_sent: 2,
# Phoenix.Live{Dashboard,View}
attr: 2,
attr: 3,
embed_templates: 1,
embed_templates: 2,
live: 2,
live: 3,
live: 4,
live_dashboard: 1,
live_dashboard: 2,
on_mount: 1,
slot: 1,
slot: 2,
slot: 3,
# Phoenix.LiveViewTest
assert_patch: 1,
assert_patch: 2,
assert_patch: 3,
assert_patched: 2,
assert_push_event: 3,
assert_push_event: 4,
assert_redirect: 1,
assert_redirect: 2,
assert_redirect: 3,
assert_redirected: 2,
assert_reply: 2,
assert_reply: 3,
refute_redirected: 1,
refute_redirected: 2,
refute_patched: 1,
refute_patched: 2,
refute_push_event: 3,
refute_push_event: 4
]
[
locals_without_parens: locals_without_parens,
export: [locals_without_parens: locals_without_parens]
]
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
### Environment
* Elixir version (elixir -v):
* Phoenix version (mix deps):
* Operating system:
### Actual behavior
### Expected behavior
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
---
blank_issues_enabled: true
contact_links:
- name: Ask questions, support, and general discussions
url: https://elixirforum.com/c/phoenix-forum
about: Ask questions, provide support, and more on Elixir Forum
- name: Propose new features
url: https://elixirforum.com/c/phoenix-forum
about: Propose new features on Elixir Forum
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
# Maintain dependencies for NPM
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "monthly"
groups:
minor-and-patch:
applies-to: version-updates
update-types:
- "minor"
- "patch"
================================================
FILE: .github/workflows/assets.yml
================================================
name: Assets
on:
push:
branches:
- main
- "v*.*"
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-24.04
env:
elixir: 1.18.3
otp: 27.2
permissions:
contents: write # for stefanzweifel/git-auto-commit-action to push code in repo
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Elixir
uses: erlef/setup-beam@3580539ceec3dc05b0ed51e9e10b08eb7a7c2bb4 # v1.21.0
with:
elixir-version: ${{ env.elixir }}
otp-version: ${{ env.otp }}
- name: Restore deps and _build cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ env.elixir }}-${{ env.otp }}-${{ hashFiles('**/mix.lock') }}-dev
restore-keys: |
${{ runner.os }}-mix-${{ env.elixir }}-${{ env.otp }}-
- name: Install Dependencies
run: mix deps.get --only dev
- name: Set up Node.js 20.x
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20.x
- name: Restore npm cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install npm dependencies
run: npm ci
- name: Build assets
run: mix assets.build
- name: Push updated assets
id: push_assets
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
with:
commit_message: Update assets
file_pattern: priv/static
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: [push, pull_request]
permissions:
contents: read
jobs:
mix_test:
name: mix test (OTP ${{matrix.otp}} | Elixir ${{matrix.elixir}})
env:
MIX_ENV: test
PHX_CI: true
strategy:
matrix:
include:
- elixir: "1.15.8"
otp: "25.3.2.9"
- elixir: "1.18.4"
otp: "27.3"
- elixir: "1.19.5"
otp: "28.3.3"
lint: true
installer: true
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Elixir
uses: erlef/setup-beam@3580539ceec3dc05b0ed51e9e10b08eb7a7c2bb4 # v1.21.0
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}
- name: Restore deps and _build cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
deps
_build
key: deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}-${{ hashFiles('**/mix.lock') }}
restore-keys: |
deps-${{ runner.os }}-${{ matrix.otp }}-${{ matrix.elixir }}
- name: Install dependencies
run: mix deps.get --only test
- name: Remove compiled application files
run: mix clean
- name: Compile & lint dependencies
run: mix compile --warnings-as-errors
if: ${{ matrix.lint }}
- name: Run tests
run: mix test
- name: Run installer test
run: |
cd installer
mix deps.get
mix test
if: ${{ matrix.installer }}
npm_test:
name: npm test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore deps and _build cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
deps
_build
key: deps-${{ runner.os }}-npm-${{ hashFiles('**/mix.lock') }}
restore-keys: |
deps-${{ runner.os }}-npm
- name: Set up Node.js 20.x
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 20.x
- name: Restore npm cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: npm install and test
run: |
cd assets
npm install
npm test
integration-test-elixir:
runs-on: ubuntu-latest
strategy:
matrix:
include:
# look for correct alpine image here: https://hub.docker.com/r/hexpm/elixir/tags
- elixir: "1.15.8"
otp: "25.3.2.9"
suffix: "alpine-3.20.3"
- elixir: "1.19.5"
otp: "28.3.3"
suffix: "alpine-3.20.9"
container:
image: hexpm/elixir:${{ matrix.elixir }}-erlang-${{ matrix.otp }}-${{ matrix.suffix }}
env:
ELIXIR_ASSERT_TIMEOUT: 10000
PHX_CI: true
services:
postgres:
image: postgres
ports:
- 5432:5432
env:
POSTGRES_PASSWORD: postgres
mysql:
image: mysql
ports:
- 3306:3306
env:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
mssql:
image: mcr.microsoft.com/mssql/server:2019-latest
env:
ACCEPT_EULA: Y
SA_PASSWORD: some!Password
ports:
- 1433:1433
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run test script
run: ./integration_test/test.sh
================================================
FILE: .github/workflows/npm-publish.yml
================================================
# https://docs.npmjs.com/trusted-publishers
name: NPM Publish
on:
push:
tags:
- "v*"
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
# Ensure npm 11.5.1 or later is installed
- name: Update npm
run: npm install -g npm@latest
- name: Determine npm tag
id: npm-tag
run: |
TAG=${GITHUB_REF#refs/tags/}
# Update this condition when bumping the major version!
if [[ $TAG == v1.7.* ]]; then
echo "tag=old-version" >> $GITHUB_OUTPUT
elif [[ $TAG == *-rc* ]]; then
echo "tag=rc" >> $GITHUB_OUTPUT
else
echo "tag=latest" >> $GITHUB_OUTPUT
fi
- name: Publish to npm
run: npm publish --tag ${{ steps.npm-tag.outputs.tag }}
================================================
FILE: .gitignore
================================================
/_build/
/deps/
/doc/
/node_modules/
/tmp/
/cover/
/assets/node_modules/
/installer/_build/
/installer/assets/
/installer/deps/
/installer/doc/
/installer/phx_new-*.ez
/installer/tmp/
/integration_test/_build/
/integration_test/deps/
erl_crash.dump
phoenix-*.ez
.DS_Store
/priv/templates/phx.gen.live/core_components.ex.eex
================================================
FILE: CHANGELOG.md
================================================
# Changelog for v1.8
This release requires Erlang/OTP 25+.
## Streamlined generators
* Extend tailwindcss support in new apps with [daisyUI](https://daisyui.com/) for light/dark/system mode support for entire app, including core components
* Simplify layout handling for new apps. Now there is only a single `root.html.heex` which wraps the render pipeline. Other dynamic layouts, like `app.html.heex` are called as needed within templates as regular function components
* Simplify core components and live generators to more closely match basic `phx.gen.html` crud. This serves as a better base for seasoned devs to start with, and lessens the amount of code newcomers need to get up to speed with on the basics
* Introduce magic links (passwordless auth) and "sudo mode" to `mix phx.gen.auth` while simplifying the generated structure
* Introduce scopes to Phoenix generators, designed to make secure data access the *default*, not something you remember (or forget) to do later
## `put_secure_browser_headers`
`put_secure_browser_headers` has been updated to the latest security practices. In particular, it sets the `content-security-policy` header to `"base-uri 'self'; frame-ancestors 'self';"` if none is set, restricting embedding of your application and the use of `` element to same origin respectively. If you expect your application to be embedded by third-parties, you want to consult the documentation.
The headers `x-download-options` and `x-frame-options` are no longer set as they have been deprecated by standards.
## Deprecations
This release introduces deprecation warnings for several features that have been soft-deprecated in the past.
* `use Phoenix.Controller` must now specify the `:formats` option, which may be set to an empty list if the formats are not known upfront
* The `:namespace` and `:put_default_views` options on `use Phoenix.Controller` are deprecated and emit a warning on use
* Specifying layouts without modules, such as `put_layout(conn, :print)` or `put_layout(conn, html: :print)` is deprecated
* The `:trailing_slash` option in `Phoenix.Router` has been deprecated in favor of using `Phoenix.VerifiedRoutes`. The overall usage of helpers will be deprecated in the future
## Potential breaking changes
* The `config` variable is no longer available in `Phoenix.Endpoint`. In the past, it was possible to read your endpoint configuration at compile-time via an injected variable named `config`, which is no longer supported. Use `Application.compile_env/3` instead, which is tracked by the Elixir compiler and lead to a better developer experience. This may also lead to errors on application boot if you were previously incorrectly setting compile time config at runtime.
## 1.8.5 (2026-03-05)
### JavaScript Client Bug Fixes
- Fix socket connecting on visibility change when never established
### Enhancements
- Fix warnings on Elixir 1.20
## 1.8.4 (2026-02-23)
### JavaScript Client Bug Fixes
- Fix bug reconnecting connections when close was gracefully initiated by server
- Fix LongPoll transport name in sessionStorage and logs
### Enhancements
- Adds guards support in `assert_push`, `assert_broadcast`, and `assert_reply`
- Enable purging in Phoenix code server for Elixir 1.20
## 1.8.3 (2025-12-08)
### Enhancements
- Add top-level phoenix config: `sort_verified_routes_query_params` to enable sorting query params in verified routes during tests
### Bug fixes
- Fix endpoint port config in an umbrella application. ([#6549](https://github.com/phoenixframework/phoenix/pull/6549))
- Drop incoming channel messages with stale join refs
## 1.8.2 (2025-11-26)
### Bug fixes
- [phoenix.js] fix issue where LongPoll can cause "unmatched topic" errors (observed on iOS only) ([#6538](https://github.com/phoenixframework/phoenix/pull/6538))
- [phx.gen.live] fix tests when schema and table names are equal ([#6477](https://github.com/phoenixframework/phoenix/pull/6477))
- [Verified Routes] do not add path prefixes for static routes
- [Phoenix.Endpoint] fix LongPoll being active by default since 1.8.0 ([#6487](https://github.com/phoenixframework/phoenix/pull/6487))
### Enhancements
- [phoenix.js] socket now stops reconnection attempts while the page is hidden ([#6534](https://github.com/phoenixframework/phoenix/pull/6534))
- [phx.new] (re-)add `<.input field={@form[:foo]} type="hidden" />` support in core components
- [phx.new] set `force_ssl` in `prod.exs` by default ([#6435](https://github.com/phoenixframework/phoenix/pull/6435))
- [phx.new] change `--docker` base image to debian trixie ([#6521](https://github.com/phoenixframework/phoenix/pull/6521))
- [Phoenix.Socket.assign/2] allow passing a function as second argument `assign(socket, fn _existing_assigns -> %{this_gets: "merged"} end)` ([#6530](https://github.com/phoenixframework/phoenix/pull/6530))
- [Phoenix.Controller.assign/2] allow passing a function as second argument ([#6542](https://github.com/phoenixframework/phoenix/pull/6542))
- [Phoenix.Controller.assign/2] support keyword lists and maps as second argument similar to LiveView ([#6513](https://github.com/phoenixframework/phoenix/pull/6513))
- [Presence] support custom dispatcher for `presence_diff` broadcast ([#6500](https://github.com/phoenixframework/phoenix/pull/6500))
- [AGENTS.md] add short test guidelines to usage rules
## 1.8.1 (2025-08-28)
### Bug fixes
- [phx.new] Fix AGENTS.md failing to include CSS and JavaScript sections
## 1.8.0 (2025-08-05)
### Bug fixes
- [phx.new] Don't include node_modules override in generated `tsconfig.json`
### Enhancements
- [phx.gen.live|html|json] - Make context argument optional. Defaults to the plural name.
- [phx.new] Add `mix precommit` alias
- [phx.new] Add `AGENTS.md` generation compatible with [`usage_rules`](https://hexdocs.pm/usage_rules/)
- [phx.new] Add `usage_rules` folder to installer, allowing to sync generic Phoenix rules into new projects
- [phx.new] Use LiveView 1.1 release in generated code
- [phx.new] Ensure theme selector and flash closing works without LiveView
## 1.8.0-rc.4 (2025-07-14)
### Bug Fixes
- Fix phx.gen.presence PubSub server name for umbrella apps
- Fix `phx.gen.live` subscribing to pubsub in disconnected mounts
### Enhancements
- [phx.new] Initialize initial git repo when git is installed
- [phx.new] Opt-in to HEEx `:debug_tags_location` in development
- [phx.gen.live|html|json|context] Make context name optional and inflect based on schema when missing
- [phx.gen.*] Use new Ecto 3.13 `Repo.transact/2` in generators
- [phx.gen.auth] Warn when using `phx.gen.auth` without esbuild as features assume `phoenix_html.js` in bundle
- Add `security.md` guide for security best practices
- [phoenix.js] - Add fetch() support to LongPoll when XMLHTTPRequest is not available
- Optimize parameter scrubbing by precompiling patterns
## 1.8.0-rc.3 (2025-05-07)
### Enhancements
- [phx.gen.auth] Allow configuring the scope's assign key in phx.gen.auth
- [phx.new] Do not override theme in root layout if explicitly set
## 1.8.0-rc.2 (2025-04-29)
### Bug Fixes
- [phx.gen.live] Only subscribe to pubsub if connected
- [phx.gen.auth] Remove unused current_password field
- [phx.gen.auth] Use context_app for scopes to fix generated scopes in umbrella apps
## 1.8.0-rc.1 (2025-04-16)
### Enhancements
- [phx.new] Support PORT in dev
- [phx.gen.auth] Replace `utc_now/0 + truncate/1` with `utc_now/1`
- [phx.gen.auth] Make dev mailbox link more obvious
### Bug Fixes
- [phx.new] Fix Tailwind custom variants for loading classes (#6194)
- [phx.new] Fix heroicons path for umbrella apps
- [phx.gen.auth] Fix missing index for scoped resources (#6186)
- [phx.gen.live] Fix crash when an open :show page gets a PubSub broadcast for items (#6197)
## 1.8.0-rc.0 (2025-04-01) 🚀
- First release candidate!
## v1.7
The CHANGELOG for v1.7 releases can be found in the [v1.7 branch](https://github.com/phoenixframework/phoenix/blob/v1.7/CHANGELOG.md).
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Code of Conduct
As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.2.0, available at [https://www.contributor-covenant.org/version/1/2/0/code-of-conduct/](https://www.contributor-covenant.org/version/1/2/0/code-of-conduct/)
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Phoenix
Please take a moment to review this document in order to make the contribution
process easy and effective for everyone involved!
Also make sure you read our [Code of Conduct](CODE_OF_CONDUCT.md) that outlines our commitment towards an open and welcoming environment.
## Using the issue tracker
Use the issues tracker for:
* [Bug reports](#bug-reports)
* [Submitting pull requests](#pull-requests)
For requesting help and discussing new features:
* [The Phoenix subforum on the Elixir Forum](https://elixirforum.com/c/phoenix-forum)
* **[#elixir](irc://irc.libera.chat/elixir)** on [Libera](https://libera.chat/) IRC
We do our best to keep the issue tracker tidy and organized, making it useful
for everyone. For example, we classify open issues per perceived difficulty,
making it easier for developers to [contribute to Phoenix](#pull-requests).
## Bug reports
A bug is either a _demonstrable problem_ that is caused by the code in the repository,
or indicates missing, unclear, or misleading documentation. Good bug reports are extremely
helpful - thank you!
Guidelines for bug reports:
1. **Use the GitHub issue search** — check if the issue has already been
reported.
2. **Check if the issue has been fixed** — try to reproduce it using the
`main` branch in the repository.
3. **Isolate and report the problem** — ideally create a reduced test
case.
Please try to be as detailed as possible in your report. Include information about
your Operating System, as well as your Erlang, Elixir and Phoenix versions. Please provide steps to
reproduce the issue as well as the outcome you were expecting! All these details
will help developers to fix any potential bugs.
Example:
> Short and descriptive example bug report title
>
> A summary of the issue and the environment in which it occurs. If suitable,
> include the steps required to reproduce the bug.
>
> 1. This is the first step
> 2. This is the second step
> 3. Further steps, etc.
>
> `` - a link to the reduced test case (e.g. a GitHub Gist)
>
> Any other information you want to share that is relevant to the issue being
> reported. This might include the lines of code that you have identified as
> causing the bug, and potential solutions (and your opinions on their
> merits).
## Feature requests
Feature requests are welcome and should be discussed on the [Phoenix subforum](https://elixirforum.com/c/phoenix-forum). But take a moment to find
out whether your idea fits with the scope and aims of the project. It's up to *you*
to make a strong case to convince the community of the merits of this feature.
Please provide as much detail and context as possible.
## Contributing Documentation
Code documentation (`@doc`, `@moduledoc`, `@typedoc`) has a special convention:
the first paragraph is considered to be a short summary.
For functions, macros, and callbacks say what it will do. For example write
something like:
```elixir
@doc """
Marks the given value as HTML safe.
"""
def safe({:safe, value}), do: {:safe, value}
```
For modules, protocols, and types say what it is. For example write
something like:
```elixir
defmodule Phoenix.HTML do
@moduledoc """
Conveniences for working HTML strings and templates.
...
"""
```
Keep in mind that the first paragraph might show up in a summary somewhere, long
texts in the first paragraph create very ugly summaries. As a rule of thumb
anything longer than 80 characters is too long.
Try to keep unnecessary details out of the first paragraph, it's only there to
give a user a quick idea of what the documented "thing" does/is. The rest of the
documentation string can contain the details, for example when a value and when
`nil` is returned.
If possible include examples, preferably in a form that works with doctests.
This makes it easy to test the examples so that they don't go stale and examples
are often a great help in explaining what a function does.
## Pull requests
Good pull requests - patches, improvements, new features - are a fantastic
help. They should remain focused in scope and avoid containing unrelated
commits.
**IMPORTANT**: By submitting a patch, you agree that your work will be
licensed under the license used by the project.
If you have any large pull request in mind (e.g. implementing features,
refactoring code, etc), **please ask first** otherwise you risk spending
a lot of time working on something that the project's developers might
not want to merge into the project.
Please adhere to the coding conventions in the project (indentation,
accurate comments, etc.) and don't forget to add your own tests and
documentation. When working with git, we recommend the following process
in order to craft an excellent pull request:
1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your fork,
and configure the remotes:
```bash
# Clone your fork of the repo into the current directory
git clone https://github.com//phoenix
# Navigate to the newly cloned directory
cd phoenix
# Assign the original repo to a remote called "upstream"
git remote add upstream https://github.com/phoenixframework/phoenix
```
2. If you cloned a while ago, get the latest changes from upstream, and update your fork:
```bash
git checkout main
git pull upstream main
git push
```
3. Create a new topic branch (off of `main`) to contain your feature, change,
or fix.
**IMPORTANT**: Making changes in `main` is discouraged. You should always
keep your local `main` in sync with upstream `main` and make your
changes in topic branches.
```bash
git checkout -b
```
4. Commit your changes in logical chunks. Keep your commit messages organized,
with a short description in the first line and more detailed information on
the following lines. Feel free to use Git's
[interactive rebase](https://help.github.com/articles/about-git-rebase/)
feature to tidy up your commits before making them public.
5. Make sure all the tests are still passing.
```bash
mix test
```
6. Push your topic branch up to your fork:
```bash
git push origin
```
7. [Open a Pull Request](https://help.github.com/articles/about-pull-requests/)
with a clear title and description.
8. If you haven't updated your pull request for a while, you should consider
rebasing on main and resolving any conflicts.
**IMPORTANT**: _Never ever_ merge upstream `main` into your branches. You
should always `git rebase` on `main` to bring your changes up to date when
necessary.
```bash
git checkout main
git pull upstream main
git checkout
git rebase main
```
Thank you for your contributions!
## Guides
These Guides aim to be inclusive. We use "we" and "our" instead of "you" and
"your" to foster this sense of inclusion.
Ideally there is something for everybody in each guide, from beginner to expert.
This is hard, maybe impossible. When we need to compromise, we do so on behalf
of beginning users because expert users have more tools at their disposal to
help themselves.
The general pattern we use for presenting information is to first introduce a
small, discrete topic, then write a small amount of code to demonstrate the
concept, then verify that the code worked.
In this way, we build from small, easily digestible concepts into more complex
ones. The shorter this cycle is, as long as the information is still clear and
complete, the better.
For formatting the guides:
- We use the `elixir` code fence for all module code.
- We use the `iex` for IEx sessions.
- We use the `console` code fence for shell commands.
- We use the `html` code fence for html templates, even if there is elixir code
in the template.
- We use backticks for filenames and directory paths.
- We use backticks for module names, function names, and variable names.
- Documentation line length should hard wrapped at around 100 characters if possible.
================================================
FILE: LICENSE.md
================================================
# MIT License
Copyright (c) 2014 Chris McCord
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
> Peace of mind from prototype to production.
[](https://github.com/phoenixframework/phoenix/actions/workflows/ci.yml) [](https://hex.pm/packages/phoenix) [](https://hexdocs.pm/phoenix)
## Getting started
See the official site at .
Install the latest version of Phoenix by following the instructions at .
## Documentation
API documentation is available at .
Phoenix.js documentation is available at .
## Contributing
We appreciate any contribution to Phoenix. Check our [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) and [CONTRIBUTING.md](CONTRIBUTING.md) guides for more information. We usually keep a list of features and bugs in the [issue tracker][4].
### Generating a Phoenix project from unreleased versions
You can create a new project using the latest Phoenix source installer (the `phx.new` Mix task) with the following steps:
1. Remove any previously installed `phx_new` archives so that Mix will pick up the local source code. This can be done with `mix archive.uninstall phx_new` or by simply deleting the file, which is usually in `~/.mix/archives/`.
2. Copy this repo via `git clone https://github.com/phoenixframework/phoenix` or by downloading it
3. Run the `phx.new` Mix task from within the `installer` directory, for example:
```bash
cd phoenix/installer
mix phx.new dev_app --dev
```
The `--dev` flag will configure your new project's `:phoenix` dep as a relative path dependency, pointing to your local Phoenix checkout:
```elixir
defp deps do
[{:phoenix, path: "../..", override: true},
```
To create projects outside of the `installer/` directory, add the latest archive to your machine by following the instructions in [installer/README.md](https://github.com/phoenixframework/phoenix/blob/main/installer/README.md)
### Building from source
To build the documentation:
```bash
npm install
MIX_ENV=docs mix docs
```
To build Phoenix:
```bash
mix deps.get
mix compile
```
To build the Phoenix installer:
```bash
mix deps.get
mix compile
mix archive.build
```
To build Phoenix.js:
```bash
cd assets
npm install
```
## Important links
* [#elixir][1] on [Libera][2] IRC
* [elixir-lang Slack channel][3]
* [Issues tracker][4]
* [Phoenix Forum (questions and proposals)][5]
* Visit Phoenix's sponsor, DockYard, for expert [Phoenix Consulting](https://dockyard.com/phoenix-consulting)
[1]: https://web.libera.chat/?channels=#elixir
[2]: https://libera.chat/
[3]: https://elixir-lang.slack.com/
[4]: https://github.com/phoenixframework/phoenix/issues
[5]: https://elixirforum.com/c/phoenix-forum
## Copyright and License
Copyright (c) 2014, Chris McCord.
Phoenix source code is licensed under the [MIT License](LICENSE.md).
================================================
FILE: RELEASE.md
================================================
# Release Instructions
1. Check related deps for required version bumps and compatibility (`phoenix_ecto`, `phoenix_html`)
2. Bump version in related files below
3. Bump external dependency version in related external files below
4. Run tests:
- `mix test` in the root folder
- `mix test` in the `installer/` folder
5. Commit, push code
6. Publish `phx_new` and `phoenix` packages and docs after pruning any extraneous uncommitted files
7. Test installer by generating a new app, running `mix deps.get`, and compiling
8. Publish to `npm` with `npm publish`
9. Update Elixir and Erlang/OTP versions on new.phoenixframework.org
10. Start -dev version in related files below
## Files with version
* `CHANGELOG`
* `mix.exs`
* `installer/mix.exs`
* `package.json`
* `assets/package.json`
## Files with external dependency versions
* `priv/templates/phx.gen.release/Docker.eex` (debian)
* `priv/templates/phx.gen.release/Docker.eex` (esbuild)
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported versions
Phoenix applies bug fixes only to the latest minor branch. Security patches are
available for the last 4 minor branches:
Phoenix version | Support
:-------------- | :-----------------------------
1.7 | Bug fixes and security patches
1.6 | Security patches only
1.5 | Security patches only
1.4 | Security patches only
## Announcements
[Security advisories will be published on GitHub](https://github.com/phoenixframework/phoenix/security).
## Reporting a vulnerability
[Please disclose security vulnerabilities privately via GitHub](https://github.com/phoenixframework/phoenix/security).
================================================
FILE: assets/js/phoenix/ajax.js
================================================
import {
global,
XHR_STATES
} from "./constants"
export default class Ajax {
static request(method, endPoint, headers, body, timeout, ontimeout, callback){
if(global.XDomainRequest){
let req = new global.XDomainRequest() // IE8, IE9
return this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback)
} else if(global.XMLHttpRequest){
let req = new global.XMLHttpRequest() // IE7+, Firefox, Chrome, Opera, Safari
return this.xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback)
} else if(global.fetch && global.AbortController){
// Fetch with AbortController for modern browsers
return this.fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback)
} else {
throw new Error("No suitable XMLHttpRequest implementation found")
}
}
static fetchRequest(method, endPoint, headers, body, timeout, ontimeout, callback){
let options = {
method,
headers,
body,
}
let controller = null
if(timeout){
controller = new AbortController()
const _timeoutId = setTimeout(() => controller.abort(), timeout)
options.signal = controller.signal
}
global.fetch(endPoint, options)
.then(response => response.text())
.then(data => this.parseJSON(data))
.then(data => callback && callback(data))
.catch(err => {
if(err.name === "AbortError" && ontimeout){
ontimeout()
} else {
callback && callback(null)
}
})
return controller
}
static xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback){
req.timeout = timeout
req.open(method, endPoint)
req.onload = () => {
let response = this.parseJSON(req.responseText)
callback && callback(response)
}
if(ontimeout){ req.ontimeout = ontimeout }
// Work around bug in IE9 that requires an attached onprogress handler
req.onprogress = () => { }
req.send(body)
return req
}
static xhrRequest(req, method, endPoint, headers, body, timeout, ontimeout, callback){
req.open(method, endPoint, true)
req.timeout = timeout
for(let [key, value] of Object.entries(headers)){
req.setRequestHeader(key, value)
}
req.onerror = () => callback && callback(null)
req.onreadystatechange = () => {
if(req.readyState === XHR_STATES.complete && callback){
let response = this.parseJSON(req.responseText)
callback(response)
}
}
if(ontimeout){ req.ontimeout = ontimeout }
req.send(body)
return req
}
static parseJSON(resp){
if(!resp || resp === ""){ return null }
try {
return JSON.parse(resp)
} catch {
console && console.log("failed to parse JSON response", resp)
return null
}
}
static serialize(obj, parentKey){
let queryStr = []
for(var key in obj){
if(!Object.prototype.hasOwnProperty.call(obj, key)){ continue }
let paramKey = parentKey ? `${parentKey}[${key}]` : key
let paramVal = obj[key]
if(typeof paramVal === "object"){
queryStr.push(this.serialize(paramVal, paramKey))
} else {
queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal))
}
}
return queryStr.join("&")
}
static appendParams(url, params){
if(Object.keys(params).length === 0){ return url }
let prefix = url.match(/\?/) ? "&" : "?"
return `${url}${prefix}${this.serialize(params)}`
}
}
================================================
FILE: assets/js/phoenix/channel.js
================================================
import {closure} from "./utils"
import {
CHANNEL_EVENTS,
CHANNEL_STATES,
} from "./constants"
import Push from "./push"
import Timer from "./timer"
/**
*
* @param {string} topic
* @param {(Object|function)} params
* @param {Socket} socket
*/
export default class Channel {
constructor(topic, params, socket){
this.state = CHANNEL_STATES.closed
this.topic = topic
this.params = closure(params || {})
this.socket = socket
this.bindings = []
this.bindingRef = 0
this.timeout = this.socket.timeout
this.joinedOnce = false
this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout)
this.pushBuffer = []
this.stateChangeRefs = []
this.rejoinTimer = new Timer(() => {
if(this.socket.isConnected()){ this.rejoin() }
}, this.socket.rejoinAfterMs)
this.stateChangeRefs.push(this.socket.onError(() => this.rejoinTimer.reset()))
this.stateChangeRefs.push(this.socket.onOpen(() => {
this.rejoinTimer.reset()
if(this.isErrored()){ this.rejoin() }
})
)
this.joinPush.receive("ok", () => {
this.state = CHANNEL_STATES.joined
this.rejoinTimer.reset()
this.pushBuffer.forEach(pushEvent => pushEvent.send())
this.pushBuffer = []
})
this.joinPush.receive("error", () => {
this.state = CHANNEL_STATES.errored
if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }
})
this.onClose(() => {
this.rejoinTimer.reset()
if(this.socket.hasLogger()) this.socket.log("channel", `close ${this.topic} ${this.joinRef()}`)
this.state = CHANNEL_STATES.closed
this.socket.remove(this)
})
this.onError(reason => {
if(this.socket.hasLogger()) this.socket.log("channel", `error ${this.topic}`, reason)
if(this.isJoining()){ this.joinPush.reset() }
this.state = CHANNEL_STATES.errored
if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }
})
this.joinPush.receive("timeout", () => {
if(this.socket.hasLogger()) this.socket.log("channel", `timeout ${this.topic} (${this.joinRef()})`, this.joinPush.timeout)
let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), this.timeout)
leavePush.send()
this.state = CHANNEL_STATES.errored
this.joinPush.reset()
if(this.socket.isConnected()){ this.rejoinTimer.scheduleTimeout() }
})
this.on(CHANNEL_EVENTS.reply, (payload, ref) => {
this.trigger(this.replyEventName(ref), payload)
})
}
/**
* Join the channel
* @param {integer} timeout
* @returns {Push}
*/
join(timeout = this.timeout){
if(this.joinedOnce){
throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance")
} else {
this.timeout = timeout
this.joinedOnce = true
this.rejoin()
return this.joinPush
}
}
/**
* Hook into channel close
* @param {Function} callback
*/
onClose(callback){
this.on(CHANNEL_EVENTS.close, callback)
}
/**
* Hook into channel errors
* @param {Function} callback
*/
onError(callback){
return this.on(CHANNEL_EVENTS.error, reason => callback(reason))
}
/**
* Subscribes on channel events
*
* Subscription returns a ref counter, which can be used later to
* unsubscribe the exact event listener
*
* @example
* const ref1 = channel.on("event", do_stuff)
* const ref2 = channel.on("event", do_other_stuff)
* channel.off("event", ref1)
* // Since unsubscription, do_stuff won't fire,
* // while do_other_stuff will keep firing on the "event"
*
* @param {string} event
* @param {Function} callback
* @returns {integer} ref
*/
on(event, callback){
let ref = this.bindingRef++
this.bindings.push({event, ref, callback})
return ref
}
/**
* Unsubscribes off of channel events
*
* Use the ref returned from a channel.on() to unsubscribe one
* handler, or pass nothing for the ref to unsubscribe all
* handlers for the given event.
*
* @example
* // Unsubscribe the do_stuff handler
* const ref1 = channel.on("event", do_stuff)
* channel.off("event", ref1)
*
* // Unsubscribe all handlers from event
* channel.off("event")
*
* @param {string} event
* @param {integer} ref
*/
off(event, ref){
this.bindings = this.bindings.filter((bind) => {
return !(bind.event === event && (typeof ref === "undefined" || ref === bind.ref))
})
}
/**
* @private
*/
canPush(){ return this.socket.isConnected() && this.isJoined() }
/**
* Sends a message `event` to phoenix with the payload `payload`.
* Phoenix receives this in the `handle_in(event, payload, socket)`
* function. if phoenix replies or it times out (default 10000ms),
* then optionally the reply can be received.
*
* @example
* channel.push("event")
* .receive("ok", payload => console.log("phoenix replied:", payload))
* .receive("error", err => console.log("phoenix errored", err))
* .receive("timeout", () => console.log("timed out pushing"))
* @param {string} event
* @param {Object} payload
* @param {number} [timeout]
* @returns {Push}
*/
push(event, payload, timeout = this.timeout){
payload = payload || {}
if(!this.joinedOnce){
throw new Error(`tried to push '${event}' to '${this.topic}' before joining. Use channel.join() before pushing events`)
}
let pushEvent = new Push(this, event, function (){ return payload }, timeout)
if(this.canPush()){
pushEvent.send()
} else {
pushEvent.startTimeout()
this.pushBuffer.push(pushEvent)
}
return pushEvent
}
/** Leaves the channel
*
* Unsubscribes from server events, and
* instructs channel to terminate on server
*
* Triggers onClose() hooks
*
* To receive leave acknowledgements, use the `receive`
* hook to bind to the server ack, ie:
*
* @example
* channel.leave().receive("ok", () => alert("left!") )
*
* @param {integer} timeout
* @returns {Push}
*/
leave(timeout = this.timeout){
this.rejoinTimer.reset()
this.joinPush.cancelTimeout()
this.state = CHANNEL_STATES.leaving
let onClose = () => {
if(this.socket.hasLogger()) this.socket.log("channel", `leave ${this.topic}`)
this.trigger(CHANNEL_EVENTS.close, "leave")
}
let leavePush = new Push(this, CHANNEL_EVENTS.leave, closure({}), timeout)
leavePush.receive("ok", () => onClose())
.receive("timeout", () => onClose())
leavePush.send()
if(!this.canPush()){ leavePush.trigger("ok", {}) }
return leavePush
}
/**
* Overridable message hook
*
* Receives all events for specialized message handling
* before dispatching to the channel callbacks.
*
* Must return the payload, modified or unmodified
* @param {string} event
* @param {Object} payload
* @param {integer} ref
* @returns {Object}
*/
onMessage(_event, payload, _ref){ return payload }
/**
* @private
*/
isMember(topic, event, payload, joinRef){
if(this.topic !== topic){ return false }
if(joinRef && joinRef !== this.joinRef()){
if(this.socket.hasLogger()) this.socket.log("channel", "dropping outdated message", {topic, event, payload, joinRef})
return false
} else {
return true
}
}
/**
* @private
*/
joinRef(){ return this.joinPush.ref }
/**
* @private
*/
rejoin(timeout = this.timeout){
if(this.isLeaving()){ return }
this.socket.leaveOpenTopic(this.topic)
this.state = CHANNEL_STATES.joining
this.joinPush.resend(timeout)
}
/**
* @private
*/
trigger(event, payload, ref, joinRef){
let handledPayload = this.onMessage(event, payload, ref, joinRef)
if(payload && !handledPayload){ throw new Error("channel onMessage callbacks must return the payload, modified or unmodified") }
let eventBindings = this.bindings.filter(bind => bind.event === event)
for(let i = 0; i < eventBindings.length; i++){
let bind = eventBindings[i]
bind.callback(handledPayload, ref, joinRef || this.joinRef())
}
}
/**
* @private
*/
replyEventName(ref){ return `chan_reply_${ref}` }
/**
* @private
*/
isClosed(){ return this.state === CHANNEL_STATES.closed }
/**
* @private
*/
isErrored(){ return this.state === CHANNEL_STATES.errored }
/**
* @private
*/
isJoined(){ return this.state === CHANNEL_STATES.joined }
/**
* @private
*/
isJoining(){ return this.state === CHANNEL_STATES.joining }
/**
* @private
*/
isLeaving(){ return this.state === CHANNEL_STATES.leaving }
}
================================================
FILE: assets/js/phoenix/constants.js
================================================
export const globalSelf = typeof self !== "undefined" ? self : null
export const phxWindow = typeof window !== "undefined" ? window : null
export const global = globalSelf || phxWindow || globalThis
export const DEFAULT_VSN = "2.0.0"
export const SOCKET_STATES = {connecting: 0, open: 1, closing: 2, closed: 3}
export const DEFAULT_TIMEOUT = 10000
export const WS_CLOSE_NORMAL = 1000
export const CHANNEL_STATES = {
closed: "closed",
errored: "errored",
joined: "joined",
joining: "joining",
leaving: "leaving",
}
export const CHANNEL_EVENTS = {
close: "phx_close",
error: "phx_error",
join: "phx_join",
reply: "phx_reply",
leave: "phx_leave"
}
export const TRANSPORTS = {
longpoll: "longpoll",
websocket: "websocket"
}
export const XHR_STATES = {
complete: 4
}
export const AUTH_TOKEN_PREFIX = "base64url.bearer.phx."
================================================
FILE: assets/js/phoenix/index.js
================================================
/**
* Phoenix Channels JavaScript client
*
* ## Socket Connection
*
* A single connection is established to the server and
* channels are multiplexed over the connection.
* Connect to the server using the `Socket` class:
*
* ```javascript
* let socket = new Socket("/socket", {params: {userToken: "123"}})
* socket.connect()
* ```
*
* The `Socket` constructor takes the mount point of the socket,
* the authentication params, as well as options that can be found in
* the Socket docs, such as configuring the `LongPoll` transport, and
* heartbeat.
*
* ## Channels
*
* Channels are isolated, concurrent processes on the server that
* subscribe to topics and broker events between the client and server.
* To join a channel, you must provide the topic, and channel params for
* authorization. Here's an example chat room example where `"new_msg"`
* events are listened for, messages are pushed to the server, and
* the channel is joined with ok/error/timeout matches:
*
* ```
* let channel = socket.channel("room:123", {token: roomToken})
* channel.on("new_msg", msg => console.log("Got message", msg) )
* $input.onEnter( e => {
* channel.push("new_msg", {body: e.target.val}, 10000)
* .receive("ok", (msg) => console.log("created message", msg) )
* .receive("error", (reasons) => console.log("create failed", reasons) )
* .receive("timeout", () => console.log("Networking issue...") )
* })
*
* channel.join()
* .receive("ok", ({messages}) => console.log("catching up", messages) )
* .receive("error", ({reason}) => console.log("failed join", reason) )
* .receive("timeout", () => console.log("Networking issue. Still waiting..."))
*```
*
* ## Joining
*
* Creating a channel with `socket.channel(topic, params)`, binds the params to
* `channel.params`, which are sent up on `channel.join()`.
* Subsequent rejoins will send up the modified params for
* updating authorization params, or passing up last_message_id information.
* Successful joins receive an "ok" status, while unsuccessful joins
* receive "error".
*
* With the default serializers and WebSocket transport, JSON text frames are
* used for pushing a JSON object literal. If an `ArrayBuffer` instance is provided,
* binary encoding will be used and the message will be sent with the binary
* opcode.
*
* *Note*: binary messages are only supported on the WebSocket transport.
*
* ## Duplicate Join Subscriptions
*
* While the client may join any number of topics on any number of channels,
* the client may only hold a single subscription for each unique topic at any
* given time. When attempting to create a duplicate subscription,
* the server will close the existing channel, log a warning, and
* spawn a new channel for the topic. The client will have their
* `channel.onClose` callbacks fired for the existing channel, and the new
* channel join will have its receive hooks processed as normal.
*
* ## Pushing Messages
*
* From the previous example, we can see that pushing messages to the server
* can be done with `channel.push(eventName, payload)` and we can optionally
* receive responses from the push. Additionally, we can use
* `receive("timeout", callback)` to abort waiting for our other `receive` hooks
* and take action after some period of waiting. The default timeout is 10000ms.
*
*
* ## Socket Hooks
*
* Lifecycle events of the multiplexed connection can be hooked into via
* `socket.onError()` and `socket.onClose()` events, ie:
*
* ```
* socket.onError( () => console.log("there was an error with the connection!") )
* socket.onClose( () => console.log("the connection dropped") )
* ```
*
*
* ## Channel Hooks
*
* For each joined channel, you can bind to `onError` and `onClose` events
* to monitor the channel lifecycle, ie:
*
* ```
* channel.onError( () => console.log("there was an error!") )
* channel.onClose( () => console.log("the channel has gone away gracefully") )
* ```
*
* ### onError hooks
*
* `onError` hooks are invoked if the socket connection drops, or the channel
* crashes on the server. In either case, a channel rejoin is attempted
* automatically in an exponential backoff manner.
*
* ### onClose hooks
*
* `onClose` hooks are invoked only in two cases. 1) the channel explicitly
* closed on the server, or 2). The client explicitly closed, by calling
* `channel.leave()`
*
*
* ## Presence
*
* The `Presence` object provides features for syncing presence information
* from the server with the client and handling presences joining and leaving.
*
* ### Syncing state from the server
*
* To sync presence state from the server, first instantiate an object and
* pass your channel in to track lifecycle events:
*
* ```
* let channel = socket.channel("some:topic")
* let presence = new Presence(channel)
* ```
*
* Next, use the `presence.onSync` callback to react to state changes
* from the server. For example, to render the list of users every time
* the list changes, you could write:
*
* ```
* presence.onSync(() => {
* myRenderUsersFunction(presence.list())
* })
* ```
*
* ### Listing Presences
*
* `presence.list` is used to return a list of presence information
* based on the local state of metadata. By default, all presence
* metadata is returned, but a `listBy` function can be supplied to
* allow the client to select which metadata to use for a given presence.
* For example, you may have a user online from different devices with
* a metadata status of "online", but they have set themselves to "away"
* on another device. In this case, the app may choose to use the "away"
* status for what appears on the UI. The example below defines a `listBy`
* function which prioritizes the first metadata which was registered for
* each user. This could be the first tab they opened, or the first device
* they came online from:
*
* ```
* let listBy = (id, {metas: [first, ...rest]}) => {
* first.count = rest.length + 1 // count of this user's presences
* first.id = id
* return first
* }
* let onlineUsers = presence.list(listBy)
* ```
*
* ### Handling individual presence join and leave events
*
* The `presence.onJoin` and `presence.onLeave` callbacks can be used to
* react to individual presences joining and leaving the app. For example:
*
* ```
* let presence = new Presence(channel)
*
* // detect if user has joined for the 1st time or from another tab/device
* presence.onJoin((id, current, newPres) => {
* if(!current){
* console.log("user has entered for the first time", newPres)
* } else {
* console.log("user additional presence", newPres)
* }
* })
*
* // detect if user has left from all tabs/devices, or is still present
* presence.onLeave((id, current, leftPres) => {
* if(current.metas.length === 0){
* console.log("user has left from all devices", leftPres)
* } else {
* console.log("user left from a device", leftPres)
* }
* })
* // receive presence data from server
* presence.onSync(() => {
* displayUsers(presence.list())
* })
* ```
* @module phoenix
*/
import Channel from "./channel"
import LongPoll from "./longpoll"
import Presence from "./presence"
import Serializer from "./serializer"
import Socket from "./socket"
export {
Channel,
LongPoll,
Presence,
Serializer,
Socket
}
================================================
FILE: assets/js/phoenix/longpoll.js
================================================
import {
SOCKET_STATES,
TRANSPORTS,
AUTH_TOKEN_PREFIX
} from "./constants"
import Ajax from "./ajax"
let arrayBufferToBase64 = (buffer) => {
let binary = ""
let bytes = new Uint8Array(buffer)
let len = bytes.byteLength
for(let i = 0; i < len; i++){ binary += String.fromCharCode(bytes[i]) }
return btoa(binary)
}
export default class LongPoll {
constructor(endPoint, protocols){
// we only support subprotocols for authToken
// ["phoenix", "base64url.bearer.phx.BASE64_ENCODED_TOKEN"]
if(protocols && protocols.length === 2 && protocols[1].startsWith(AUTH_TOKEN_PREFIX)){
this.authToken = atob(protocols[1].slice(AUTH_TOKEN_PREFIX.length))
}
this.endPoint = null
this.token = null
this.skipHeartbeat = true
this.reqs = new Set()
this.awaitingBatchAck = false
this.currentBatch = null
this.currentBatchTimer = null
this.batchBuffer = []
this.onopen = function (){ } // noop
this.onerror = function (){ } // noop
this.onmessage = function (){ } // noop
this.onclose = function (){ } // noop
this.pollEndpoint = this.normalizeEndpoint(endPoint)
this.readyState = SOCKET_STATES.connecting
// we must wait for the caller to finish setting up our callbacks and timeout properties
setTimeout(() => this.poll(), 0)
}
normalizeEndpoint(endPoint){
return (endPoint
.replace("ws://", "http://")
.replace("wss://", "https://")
.replace(new RegExp("(.*)\/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll))
}
endpointURL(){
return Ajax.appendParams(this.pollEndpoint, {token: this.token})
}
closeAndRetry(code, reason, wasClean){
this.close(code, reason, wasClean)
this.readyState = SOCKET_STATES.connecting
}
ontimeout(){
this.onerror("timeout")
this.closeAndRetry(1005, "timeout", false)
}
isActive(){ return this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting }
poll(){
const headers = {"Accept": "application/json"}
if(this.authToken){
headers["X-Phoenix-AuthToken"] = this.authToken
}
this.ajax("GET", headers, null, () => this.ontimeout(), resp => {
if(resp){
var {status, token, messages} = resp
if(status === 410 && this.token !== null){
// In case we already have a token, this means that our existing session
// is gone. We fail so that the client rejoins its channels.
this.onerror(410)
this.closeAndRetry(3410, "session_gone", false)
return
}
this.token = token
} else {
status = 0
}
switch(status){
case 200:
messages.forEach(msg => {
// Tasks are what things like event handlers, setTimeout callbacks,
// promise resolves and more are run within.
// In modern browsers, there are two different kinds of tasks,
// microtasks and macrotasks.
// Microtasks are mainly used for Promises, while macrotasks are
// used for everything else.
// Microtasks always have priority over macrotasks. If the JS engine
// is looking for a task to run, it will always try to empty the
// microtask queue before attempting to run anything from the
// macrotask queue.
//
// For the WebSocket transport, messages always arrive in their own
// event. This means that if any promises are resolved from within,
// their callbacks will always finish execution by the time the
// next message event handler is run.
//
// In order to emulate this behaviour, we need to make sure each
// onmessage handler is run within its own macrotask.
setTimeout(() => this.onmessage({data: msg}), 0)
})
this.poll()
break
case 204:
this.poll()
break
case 410:
this.readyState = SOCKET_STATES.open
this.onopen({})
this.poll()
break
case 403:
this.onerror(403)
this.close(1008, "forbidden", false)
break
case 0:
case 500:
this.onerror(500)
this.closeAndRetry(1011, "internal server error", 500)
break
default: throw new Error(`unhandled poll status ${status}`)
}
})
}
// we collect all pushes within the current event loop by
// setTimeout 0, which optimizes back-to-back procedural
// pushes against an empty buffer
send(body){
if(typeof(body) !== "string"){ body = arrayBufferToBase64(body) }
if(this.currentBatch){
this.currentBatch.push(body)
} else if(this.awaitingBatchAck){
this.batchBuffer.push(body)
} else {
this.currentBatch = [body]
this.currentBatchTimer = setTimeout(() => {
this.batchSend(this.currentBatch)
this.currentBatch = null
}, 0)
}
}
batchSend(messages){
this.awaitingBatchAck = true
this.ajax("POST", {"Content-Type": "application/x-ndjson"}, messages.join("\n"), () => this.onerror("timeout"), resp => {
this.awaitingBatchAck = false
if(!resp || resp.status !== 200){
this.onerror(resp && resp.status)
this.closeAndRetry(1011, "internal server error", false)
} else if(this.batchBuffer.length > 0){
this.batchSend(this.batchBuffer)
this.batchBuffer = []
}
})
}
close(code, reason, wasClean){
for(let req of this.reqs){ req.abort() }
this.readyState = SOCKET_STATES.closed
let opts = Object.assign({code: 1000, reason: undefined, wasClean: true}, {code, reason, wasClean})
this.batchBuffer = []
clearTimeout(this.currentBatchTimer)
this.currentBatchTimer = null
if(typeof(CloseEvent) !== "undefined"){
this.onclose(new CloseEvent("close", opts))
} else {
this.onclose(opts)
}
}
ajax(method, headers, body, onCallerTimeout, callback){
let req
let ontimeout = () => {
this.reqs.delete(req)
onCallerTimeout()
}
req = Ajax.request(method, this.endpointURL(), headers, body, this.timeout, ontimeout, resp => {
this.reqs.delete(req)
if(this.isActive()){ callback(resp) }
})
this.reqs.add(req)
}
}
================================================
FILE: assets/js/phoenix/presence.js
================================================
/**
* Initializes the Presence
* @param {Channel} channel - The Channel
* @param {Object} opts - The options,
* for example `{events: {state: "state", diff: "diff"}}`
*/
export default class Presence {
constructor(channel, opts = {}){
let events = opts.events || {state: "presence_state", diff: "presence_diff"}
this.state = {}
this.pendingDiffs = []
this.channel = channel
this.joinRef = null
this.caller = {
onJoin: function (){ },
onLeave: function (){ },
onSync: function (){ }
}
this.channel.on(events.state, newState => {
let {onJoin, onLeave, onSync} = this.caller
this.joinRef = this.channel.joinRef()
this.state = Presence.syncState(this.state, newState, onJoin, onLeave)
this.pendingDiffs.forEach(diff => {
this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)
})
this.pendingDiffs = []
onSync()
})
this.channel.on(events.diff, diff => {
let {onJoin, onLeave, onSync} = this.caller
if(this.inPendingSyncState()){
this.pendingDiffs.push(diff)
} else {
this.state = Presence.syncDiff(this.state, diff, onJoin, onLeave)
onSync()
}
})
}
onJoin(callback){ this.caller.onJoin = callback }
onLeave(callback){ this.caller.onLeave = callback }
onSync(callback){ this.caller.onSync = callback }
list(by){ return Presence.list(this.state, by) }
inPendingSyncState(){
return !this.joinRef || (this.joinRef !== this.channel.joinRef())
}
// lower-level public static API
/**
* Used to sync the list of presences on the server
* with the client's state. An optional `onJoin` and `onLeave` callback can
* be provided to react to changes in the client's local presences across
* disconnects and reconnects with the server.
*
* @returns {Presence}
*/
static syncState(currentState, newState, onJoin, onLeave){
let state = this.clone(currentState)
let joins = {}
let leaves = {}
this.map(state, (key, presence) => {
if(!newState[key]){
leaves[key] = presence
}
})
this.map(newState, (key, newPresence) => {
let currentPresence = state[key]
if(currentPresence){
let newRefs = newPresence.metas.map(m => m.phx_ref)
let curRefs = currentPresence.metas.map(m => m.phx_ref)
let joinedMetas = newPresence.metas.filter(m => curRefs.indexOf(m.phx_ref) < 0)
let leftMetas = currentPresence.metas.filter(m => newRefs.indexOf(m.phx_ref) < 0)
if(joinedMetas.length > 0){
joins[key] = newPresence
joins[key].metas = joinedMetas
}
if(leftMetas.length > 0){
leaves[key] = this.clone(currentPresence)
leaves[key].metas = leftMetas
}
} else {
joins[key] = newPresence
}
})
return this.syncDiff(state, {joins: joins, leaves: leaves}, onJoin, onLeave)
}
/**
*
* Used to sync a diff of presence join and leave
* events from the server, as they happen. Like `syncState`, `syncDiff`
* accepts optional `onJoin` and `onLeave` callbacks to react to a user
* joining or leaving from a device.
*
* @returns {Presence}
*/
static syncDiff(state, diff, onJoin, onLeave){
let {joins, leaves} = this.clone(diff)
if(!onJoin){ onJoin = function (){ } }
if(!onLeave){ onLeave = function (){ } }
this.map(joins, (key, newPresence) => {
let currentPresence = state[key]
state[key] = this.clone(newPresence)
if(currentPresence){
let joinedRefs = state[key].metas.map(m => m.phx_ref)
let curMetas = currentPresence.metas.filter(m => joinedRefs.indexOf(m.phx_ref) < 0)
state[key].metas.unshift(...curMetas)
}
onJoin(key, currentPresence, newPresence)
})
this.map(leaves, (key, leftPresence) => {
let currentPresence = state[key]
if(!currentPresence){ return }
let refsToRemove = leftPresence.metas.map(m => m.phx_ref)
currentPresence.metas = currentPresence.metas.filter(p => {
return refsToRemove.indexOf(p.phx_ref) < 0
})
onLeave(key, currentPresence, leftPresence)
if(currentPresence.metas.length === 0){
delete state[key]
}
})
return state
}
/**
* Returns the array of presences, with selected metadata.
*
* @param {Object} presences
* @param {Function} chooser
*
* @returns {Presence}
*/
static list(presences, chooser){
if(!chooser){ chooser = function (key, pres){ return pres } }
return this.map(presences, (key, presence) => {
return chooser(key, presence)
})
}
// private
static map(obj, func){
return Object.getOwnPropertyNames(obj).map(key => func(key, obj[key]))
}
static clone(obj){ return JSON.parse(JSON.stringify(obj)) }
}
================================================
FILE: assets/js/phoenix/push.js
================================================
/**
* Initializes the Push
* @param {Channel} channel - The Channel
* @param {string} event - The event, for example `"phx_join"`
* @param {Object} payload - The payload, for example `{user_id: 123}`
* @param {number} timeout - The push timeout in milliseconds
*/
export default class Push {
constructor(channel, event, payload, timeout){
this.channel = channel
this.event = event
this.payload = payload || function (){ return {} }
this.receivedResp = null
this.timeout = timeout
this.timeoutTimer = null
this.recHooks = []
this.sent = false
}
/**
*
* @param {number} timeout
*/
resend(timeout){
this.timeout = timeout
this.reset()
this.send()
}
/**
*
*/
send(){
if(this.hasReceived("timeout")){ return }
this.startTimeout()
this.sent = true
this.channel.socket.push({
topic: this.channel.topic,
event: this.event,
payload: this.payload(),
ref: this.ref,
join_ref: this.channel.joinRef()
})
}
/**
*
* @param {*} status
* @param {*} callback
*/
receive(status, callback){
if(this.hasReceived(status)){
callback(this.receivedResp.response)
}
this.recHooks.push({status, callback})
return this
}
/**
* @private
*/
reset(){
this.cancelRefEvent()
this.ref = null
this.refEvent = null
this.receivedResp = null
this.sent = false
}
/**
* @private
*/
matchReceive({status, response, _ref}){
this.recHooks.filter(h => h.status === status)
.forEach(h => h.callback(response))
}
/**
* @private
*/
cancelRefEvent(){
if(!this.refEvent){ return }
this.channel.off(this.refEvent)
}
/**
* @private
*/
cancelTimeout(){
clearTimeout(this.timeoutTimer)
this.timeoutTimer = null
}
/**
* @private
*/
startTimeout(){
if(this.timeoutTimer){ this.cancelTimeout() }
this.ref = this.channel.socket.makeRef()
this.refEvent = this.channel.replyEventName(this.ref)
this.channel.on(this.refEvent, payload => {
this.cancelRefEvent()
this.cancelTimeout()
this.receivedResp = payload
this.matchReceive(payload)
})
this.timeoutTimer = setTimeout(() => {
this.trigger("timeout", {})
}, this.timeout)
}
/**
* @private
*/
hasReceived(status){
return this.receivedResp && this.receivedResp.status === status
}
/**
* @private
*/
trigger(status, response){
this.channel.trigger(this.refEvent, {status, response})
}
}
================================================
FILE: assets/js/phoenix/serializer.js
================================================
/* The default serializer for encoding and decoding messages */
import {
CHANNEL_EVENTS
} from "./constants"
export default {
HEADER_LENGTH: 1,
META_LENGTH: 4,
KINDS: {push: 0, reply: 1, broadcast: 2},
encode(msg, callback){
if(msg.payload.constructor === ArrayBuffer){
return callback(this.binaryEncode(msg))
} else {
let payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload]
return callback(JSON.stringify(payload))
}
},
decode(rawPayload, callback){
if(rawPayload.constructor === ArrayBuffer){
return callback(this.binaryDecode(rawPayload))
} else {
let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload)
return callback({join_ref, ref, topic, event, payload})
}
},
// private
binaryEncode(message){
let {join_ref, ref, event, topic, payload} = message
let metaLength = this.META_LENGTH + join_ref.length + ref.length + topic.length + event.length
let header = new ArrayBuffer(this.HEADER_LENGTH + metaLength)
let view = new DataView(header)
let offset = 0
view.setUint8(offset++, this.KINDS.push) // kind
view.setUint8(offset++, join_ref.length)
view.setUint8(offset++, ref.length)
view.setUint8(offset++, topic.length)
view.setUint8(offset++, event.length)
Array.from(join_ref, char => view.setUint8(offset++, char.charCodeAt(0)))
Array.from(ref, char => view.setUint8(offset++, char.charCodeAt(0)))
Array.from(topic, char => view.setUint8(offset++, char.charCodeAt(0)))
Array.from(event, char => view.setUint8(offset++, char.charCodeAt(0)))
var combined = new Uint8Array(header.byteLength + payload.byteLength)
combined.set(new Uint8Array(header), 0)
combined.set(new Uint8Array(payload), header.byteLength)
return combined.buffer
},
binaryDecode(buffer){
let view = new DataView(buffer)
let kind = view.getUint8(0)
let decoder = new TextDecoder()
switch(kind){
case this.KINDS.push: return this.decodePush(buffer, view, decoder)
case this.KINDS.reply: return this.decodeReply(buffer, view, decoder)
case this.KINDS.broadcast: return this.decodeBroadcast(buffer, view, decoder)
}
},
decodePush(buffer, view, decoder){
let joinRefSize = view.getUint8(1)
let topicSize = view.getUint8(2)
let eventSize = view.getUint8(3)
let offset = this.HEADER_LENGTH + this.META_LENGTH - 1 // pushes have no ref
let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))
offset = offset + joinRefSize
let topic = decoder.decode(buffer.slice(offset, offset + topicSize))
offset = offset + topicSize
let event = decoder.decode(buffer.slice(offset, offset + eventSize))
offset = offset + eventSize
let data = buffer.slice(offset, buffer.byteLength)
return {join_ref: joinRef, ref: null, topic: topic, event: event, payload: data}
},
decodeReply(buffer, view, decoder){
let joinRefSize = view.getUint8(1)
let refSize = view.getUint8(2)
let topicSize = view.getUint8(3)
let eventSize = view.getUint8(4)
let offset = this.HEADER_LENGTH + this.META_LENGTH
let joinRef = decoder.decode(buffer.slice(offset, offset + joinRefSize))
offset = offset + joinRefSize
let ref = decoder.decode(buffer.slice(offset, offset + refSize))
offset = offset + refSize
let topic = decoder.decode(buffer.slice(offset, offset + topicSize))
offset = offset + topicSize
let event = decoder.decode(buffer.slice(offset, offset + eventSize))
offset = offset + eventSize
let data = buffer.slice(offset, buffer.byteLength)
let payload = {status: event, response: data}
return {join_ref: joinRef, ref: ref, topic: topic, event: CHANNEL_EVENTS.reply, payload: payload}
},
decodeBroadcast(buffer, view, decoder){
let topicSize = view.getUint8(1)
let eventSize = view.getUint8(2)
let offset = this.HEADER_LENGTH + 2
let topic = decoder.decode(buffer.slice(offset, offset + topicSize))
offset = offset + topicSize
let event = decoder.decode(buffer.slice(offset, offset + eventSize))
offset = offset + eventSize
let data = buffer.slice(offset, buffer.byteLength)
return {join_ref: null, ref: null, topic: topic, event: event, payload: data}
}
}
================================================
FILE: assets/js/phoenix/socket.js
================================================
import {
global,
phxWindow,
CHANNEL_EVENTS,
DEFAULT_TIMEOUT,
DEFAULT_VSN,
SOCKET_STATES,
TRANSPORTS,
WS_CLOSE_NORMAL,
AUTH_TOKEN_PREFIX
} from "./constants"
import {
closure
} from "./utils"
import Ajax from "./ajax"
import Channel from "./channel"
import LongPoll from "./longpoll"
import Serializer from "./serializer"
import Timer from "./timer"
/** Initializes the Socket *
*
* For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
*
* @param {string} endPoint - The string WebSocket endpoint, ie, `"ws://example.com/socket"`,
* `"wss://example.com"`
* `"/socket"` (inherited host & protocol)
* @param {Object} [opts] - Optional configuration
* @param {Function} [opts.transport] - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.
*
* Defaults to WebSocket with automatic LongPoll fallback if WebSocket is not defined.
* To fallback to LongPoll when WebSocket attempts fail, use `longPollFallbackMs: 2500`.
*
* @param {number} [opts.longPollFallbackMs] - The millisecond time to attempt the primary transport
* before falling back to the LongPoll transport. Disabled by default.
*
* @param {boolean} [opts.debug] - When true, enables debug logging. Default false.
*
* @param {Function} [opts.encode] - The function to encode outgoing messages.
*
* Defaults to JSON encoder.
*
* @param {Function} [opts.decode] - The function to decode incoming messages.
*
* Defaults to JSON:
*
* ```javascript
* (payload, callback) => callback(JSON.parse(payload))
* ```
*
* @param {number} [opts.timeout] - The default timeout in milliseconds to trigger push timeouts.
*
* Defaults `DEFAULT_TIMEOUT`
* @param {number} [opts.heartbeatIntervalMs] - The millisec interval to send a heartbeat message
* @param {Function} [opts.reconnectAfterMs] - The optional function that returns the
* socket reconnect interval, in milliseconds.
*
* Defaults to stepped backoff of:
*
* ```javascript
* function(tries){
* return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000
* }
* ````
*
* @param {Function} [opts.rejoinAfterMs] - The optional function that returns the millisec
* rejoin interval for individual channels.
*
* ```javascript
* function(tries){
* return [1000, 2000, 5000][tries - 1] || 10000
* }
* ````
*
* @param {Function} [opts.logger] - The optional function for specialized logging, ie:
*
* ```javascript
* function(kind, msg, data) {
* console.log(`${kind}: ${msg}`, data)
* }
* ```
*
* @param {number} [opts.longpollerTimeout] - The maximum timeout of a long poll AJAX request.
*
* Defaults to 20s (double the server long poll timer).
*
* @param {(Object|function)} [opts.params] - The optional params to pass when connecting
* @param {string} [opts.authToken] - the optional authentication token to be exposed on the server
* under the `:auth_token` connect_info key.
* @param {string} [opts.binaryType] - The binary type to use for binary WebSocket frames.
*
* Defaults to "arraybuffer"
*
* @param {vsn} [opts.vsn] - The serializer's protocol version to send on connect.
*
* Defaults to DEFAULT_VSN.
*
* @param {Object} [opts.sessionStorage] - An optional Storage compatible object
* Phoenix uses sessionStorage for longpoll fallback history. Overriding the store is
* useful when Phoenix won't have access to `sessionStorage`. For example, This could
* happen if a site loads a cross-domain channel in an iframe. Example usage:
*
* class InMemoryStorage {
* constructor() { this.storage = {} }
* getItem(keyName) { return this.storage[keyName] || null }
* removeItem(keyName) { delete this.storage[keyName] }
* setItem(keyName, keyValue) { this.storage[keyName] = keyValue }
* }
*
*/
export default class Socket {
constructor(endPoint, opts = {}){
this.stateChangeCallbacks = {open: [], close: [], error: [], message: []}
this.channels = []
this.sendBuffer = []
this.ref = 0
this.fallbackRef = null
this.timeout = opts.timeout || DEFAULT_TIMEOUT
this.transport = opts.transport || global.WebSocket || LongPoll
this.primaryPassedHealthCheck = false
this.longPollFallbackMs = opts.longPollFallbackMs
this.fallbackTimer = null
this.sessionStore = opts.sessionStorage || (global && global.sessionStorage)
this.establishedConnections = 0
this.defaultEncoder = Serializer.encode.bind(Serializer)
this.defaultDecoder = Serializer.decode.bind(Serializer)
// We start with closeWasClean true to avoid the visibility change
// logic from connecting if the socket was never connected in the first place.
// transportConnect sets it to false on open.
this.closeWasClean = true
this.disconnecting = false
this.binaryType = opts.binaryType || "arraybuffer"
this.connectClock = 1
this.pageHidden = false
if(this.transport !== LongPoll){
this.encode = opts.encode || this.defaultEncoder
this.decode = opts.decode || this.defaultDecoder
} else {
this.encode = this.defaultEncoder
this.decode = this.defaultDecoder
}
let awaitingConnectionOnPageShow = null
if(phxWindow && phxWindow.addEventListener){
phxWindow.addEventListener("pagehide", _e => {
if(this.conn){
this.disconnect()
awaitingConnectionOnPageShow = this.connectClock
}
})
phxWindow.addEventListener("pageshow", _e => {
if(awaitingConnectionOnPageShow === this.connectClock){
awaitingConnectionOnPageShow = null
this.connect()
}
})
phxWindow.addEventListener("visibilitychange", () => {
if(document.visibilityState === "hidden"){
this.pageHidden = true
} else {
this.pageHidden = false
// reconnect immediately
if(!this.isConnected() && !this.closeWasClean){
this.teardown(() => this.connect())
}
}
})
}
this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000
this.rejoinAfterMs = (tries) => {
if(opts.rejoinAfterMs){
return opts.rejoinAfterMs(tries)
} else {
return [1000, 2000, 5000][tries - 1] || 10000
}
}
this.reconnectAfterMs = (tries) => {
if(opts.reconnectAfterMs){
return opts.reconnectAfterMs(tries)
} else {
return [10, 50, 100, 150, 200, 250, 500, 1000, 2000][tries - 1] || 5000
}
}
this.logger = opts.logger || null
if(!this.logger && opts.debug){
this.logger = (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
}
this.longpollerTimeout = opts.longpollerTimeout || 20000
this.params = closure(opts.params || {})
this.endPoint = `${endPoint}/${TRANSPORTS.websocket}`
this.vsn = opts.vsn || DEFAULT_VSN
this.heartbeatTimeoutTimer = null
this.heartbeatTimer = null
this.pendingHeartbeatRef = null
this.reconnectTimer = new Timer(() => {
if(this.pageHidden){
this.log("Not reconnecting as page is hidden!")
this.teardown()
return
}
this.teardown(() => this.connect())
}, this.reconnectAfterMs)
this.authToken = opts.authToken
}
/**
* Returns the LongPoll transport reference
*/
getLongPollTransport(){ return LongPoll }
/**
* Disconnects and replaces the active transport
*
* @param {Function} newTransport - The new transport class to instantiate
*
*/
replaceTransport(newTransport){
this.connectClock++
this.closeWasClean = true
clearTimeout(this.fallbackTimer)
this.reconnectTimer.reset()
if(this.conn){
this.conn.close()
this.conn = null
}
this.transport = newTransport
}
/**
* Returns the socket protocol
*
* @returns {string}
*/
protocol(){ return location.protocol.match(/^https/) ? "wss" : "ws" }
/**
* The fully qualified socket url
*
* @returns {string}
*/
endPointURL(){
let uri = Ajax.appendParams(
Ajax.appendParams(this.endPoint, this.params()), {vsn: this.vsn})
if(uri.charAt(0) !== "/"){ return uri }
if(uri.charAt(1) === "/"){ return `${this.protocol()}:${uri}` }
return `${this.protocol()}://${location.host}${uri}`
}
/**
* Disconnects the socket
*
* See https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes for valid status codes.
*
* @param {Function} callback - Optional callback which is called after socket is disconnected.
* @param {integer} code - A status code for disconnection (Optional).
* @param {string} reason - A textual description of the reason to disconnect. (Optional)
*/
disconnect(callback, code, reason){
this.connectClock++
this.disconnecting = true
this.closeWasClean = true
clearTimeout(this.fallbackTimer)
this.reconnectTimer.reset()
this.teardown(() => {
this.disconnecting = false
callback && callback()
}, code, reason)
}
/**
*
* @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`
*
* Passing params to connect is deprecated; pass them in the Socket constructor instead:
* `new Socket("/socket", {params: {user_id: userToken}})`.
*/
connect(params){
if(params){
console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor")
this.params = closure(params)
}
if(this.conn && !this.disconnecting){ return }
if(this.longPollFallbackMs && this.transport !== LongPoll){
this.connectWithFallback(LongPoll, this.longPollFallbackMs)
} else {
this.transportConnect()
}
}
/**
* Logs the message. Override `this.logger` for specialized logging. noops by default
* @param {string} kind
* @param {string} msg
* @param {Object} data
*/
log(kind, msg, data){ this.logger && this.logger(kind, msg, data) }
/**
* Returns true if a logger has been set on this socket.
*/
hasLogger(){ return this.logger !== null }
/**
* Registers callbacks for connection open events
*
* @example socket.onOpen(function(){ console.info("the socket was opened") })
*
* @param {Function} callback
*/
onOpen(callback){
let ref = this.makeRef()
this.stateChangeCallbacks.open.push([ref, callback])
return ref
}
/**
* Registers callbacks for connection close events
* @param {Function} callback
*/
onClose(callback){
let ref = this.makeRef()
this.stateChangeCallbacks.close.push([ref, callback])
return ref
}
/**
* Registers callbacks for connection error events
*
* @example socket.onError(function(error){ alert("An error occurred") })
*
* @param {Function} callback
*/
onError(callback){
let ref = this.makeRef()
this.stateChangeCallbacks.error.push([ref, callback])
return ref
}
/**
* Registers callbacks for connection message events
* @param {Function} callback
*/
onMessage(callback){
let ref = this.makeRef()
this.stateChangeCallbacks.message.push([ref, callback])
return ref
}
/**
* Pings the server and invokes the callback with the RTT in milliseconds
* @param {Function} callback
*
* Returns true if the ping was pushed or false if unable to be pushed.
*/
ping(callback){
if(!this.isConnected()){ return false }
let ref = this.makeRef()
let startTime = Date.now()
this.push({topic: "phoenix", event: "heartbeat", payload: {}, ref: ref})
let onMsgRef = this.onMessage(msg => {
if(msg.ref === ref){
this.off([onMsgRef])
callback(Date.now() - startTime)
}
})
return true
}
/**
* @private
*
* @param {Function}
*/
transportName(transport){
// JavaScript minification, enabled by default in production in Phoenix
// projects, renames symbols to reduce code size.
// See https://esbuild.github.io/api/#keep-names.
// This helper ensures we return the correct name for the LongPoll transport
// even after minification. The other common transport is WebSocket, which
// is native to browsers and does not need special handling.
switch(transport){
case LongPoll: return "LongPoll"
default: return transport.name
}
}
/**
* @private
*/
transportConnect(){
this.connectClock++
this.closeWasClean = false
let protocols = undefined
// Sec-WebSocket-Protocol based token
// (longpoll uses Authorization header instead)
if(this.authToken){
protocols = ["phoenix", `${AUTH_TOKEN_PREFIX}${btoa(this.authToken).replace(/=/g, "")}`]
}
this.conn = new this.transport(this.endPointURL(), protocols)
this.conn.binaryType = this.binaryType
this.conn.timeout = this.longpollerTimeout
this.conn.onopen = () => this.onConnOpen()
this.conn.onerror = error => this.onConnError(error)
this.conn.onmessage = event => this.onConnMessage(event)
this.conn.onclose = event => this.onConnClose(event)
}
getSession(key){ return this.sessionStore && this.sessionStore.getItem(key) }
storeSession(key, val){ this.sessionStore && this.sessionStore.setItem(key, val) }
connectWithFallback(fallbackTransport, fallbackThreshold = 2500){
clearTimeout(this.fallbackTimer)
let established = false
let primaryTransport = true
let openRef, errorRef
let fallbackTransportName = this.transportName(fallbackTransport)
let fallback = (reason) => {
this.log("transport", `falling back to ${fallbackTransportName}...`, reason)
this.off([openRef, errorRef])
primaryTransport = false
this.replaceTransport(fallbackTransport)
this.transportConnect()
}
if(this.getSession(`phx:fallback:${fallbackTransportName}`)){ return fallback("memorized") }
this.fallbackTimer = setTimeout(fallback, fallbackThreshold)
errorRef = this.onError(reason => {
this.log("transport", "error", reason)
if(primaryTransport && !established){
clearTimeout(this.fallbackTimer)
fallback(reason)
}
})
if(this.fallbackRef){
this.off([this.fallbackRef])
}
this.fallbackRef = this.onOpen(() => {
established = true
if(!primaryTransport){
let fallbackTransportName = this.transportName(fallbackTransport)
// only memorize LP if we never connected to primary
if(!this.primaryPassedHealthCheck){ this.storeSession(`phx:fallback:${fallbackTransportName}`, "true") }
return this.log("transport", `established ${fallbackTransportName} fallback`)
}
// if we've established primary, give the fallback a new period to attempt ping
clearTimeout(this.fallbackTimer)
this.fallbackTimer = setTimeout(fallback, fallbackThreshold)
this.ping(rtt => {
this.log("transport", "connected to primary after", rtt)
this.primaryPassedHealthCheck = true
clearTimeout(this.fallbackTimer)
})
})
this.transportConnect()
}
clearHeartbeats(){
clearTimeout(this.heartbeatTimer)
clearTimeout(this.heartbeatTimeoutTimer)
}
onConnOpen(){
if(this.hasLogger()) this.log("transport", `${this.transportName(this.transport)} connected to ${this.endPointURL()}`)
this.closeWasClean = false
this.disconnecting = false
this.establishedConnections++
this.flushSendBuffer()
this.reconnectTimer.reset()
this.resetHeartbeat()
this.stateChangeCallbacks.open.forEach(([, callback]) => callback())
}
/**
* @private
*/
heartbeatTimeout(){
if(this.pendingHeartbeatRef){
this.pendingHeartbeatRef = null
if(this.hasLogger()){ this.log("transport", "heartbeat timeout. Attempting to re-establish connection") }
this.triggerChanError()
this.closeWasClean = false
this.teardown(() => this.reconnectTimer.scheduleTimeout(), WS_CLOSE_NORMAL, "heartbeat timeout")
}
}
resetHeartbeat(){
if(this.conn && this.conn.skipHeartbeat){ return }
this.pendingHeartbeatRef = null
this.clearHeartbeats()
this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
}
teardown(callback, code, reason){
if(!this.conn){
return callback && callback()
}
// If someone calls connect before we finish tearing down,
// we create a new connection, but we still want to finish tearing down the old one.
const connToClose = this.conn
this.waitForBufferDone(connToClose, () => {
if(code){ connToClose.close(code, reason || "") } else { connToClose.close() }
this.waitForSocketClosed(connToClose, () => {
if(this.conn === connToClose){
this.conn.onopen = function (){ } // noop
this.conn.onerror = function (){ } // noop
this.conn.onmessage = function (){ } // noop
this.conn.onclose = function (){ } // noop
this.conn = null
}
callback && callback()
})
})
}
waitForBufferDone(conn, callback, tries = 1){
if(tries === 5 || !conn.bufferedAmount){
callback()
return
}
setTimeout(() => {
this.waitForBufferDone(conn, callback, tries + 1)
}, 150 * tries)
}
waitForSocketClosed(conn, callback, tries = 1){
if(tries === 5 || conn.readyState === SOCKET_STATES.closed){
callback()
return
}
setTimeout(() => {
this.waitForSocketClosed(conn, callback, tries + 1)
}, 150 * tries)
}
onConnClose(event){
if(this.conn) this.conn.onclose = () => {} // noop to prevent recursive calls in teardown
let closeCode = event && event.code
if(this.hasLogger()) this.log("transport", "close", event)
this.triggerChanError()
this.clearHeartbeats()
if(!this.closeWasClean && closeCode !== 1000){
this.reconnectTimer.scheduleTimeout()
}
this.stateChangeCallbacks.close.forEach(([, callback]) => callback(event))
}
/**
* @private
*/
onConnError(error){
if(this.hasLogger()) this.log("transport", error)
let transportBefore = this.transport
let establishedBefore = this.establishedConnections
this.stateChangeCallbacks.error.forEach(([, callback]) => {
callback(error, transportBefore, establishedBefore)
})
if(transportBefore === this.transport || establishedBefore > 0){
this.triggerChanError()
}
}
/**
* @private
*/
triggerChanError(){
this.channels.forEach(channel => {
if(!(channel.isErrored() || channel.isLeaving() || channel.isClosed())){
channel.trigger(CHANNEL_EVENTS.error)
}
})
}
/**
* @returns {string}
*/
connectionState(){
switch(this.conn && this.conn.readyState){
case SOCKET_STATES.connecting: return "connecting"
case SOCKET_STATES.open: return "open"
case SOCKET_STATES.closing: return "closing"
default: return "closed"
}
}
/**
* @returns {boolean}
*/
isConnected(){ return this.connectionState() === "open" }
/**
* @private
*
* @param {Channel}
*/
remove(channel){
this.off(channel.stateChangeRefs)
this.channels = this.channels.filter(c => c !== channel)
}
/**
* Removes `onOpen`, `onClose`, `onError,` and `onMessage` registrations.
*
* @param {refs} - list of refs returned by calls to
* `onOpen`, `onClose`, `onError,` and `onMessage`
*/
off(refs){
for(let key in this.stateChangeCallbacks){
this.stateChangeCallbacks[key] = this.stateChangeCallbacks[key].filter(([ref]) => {
return refs.indexOf(ref) === -1
})
}
}
/**
* Initiates a new channel for the given topic
*
* @param {string} topic
* @param {Object} chanParams - Parameters for the channel
* @returns {Channel}
*/
channel(topic, chanParams = {}){
let chan = new Channel(topic, chanParams, this)
this.channels.push(chan)
return chan
}
/**
* @param {Object} data
*/
push(data){
if(this.hasLogger()){
let {topic, event, payload, ref, join_ref} = data
this.log("push", `${topic} ${event} (${join_ref}, ${ref})`, payload)
}
if(this.isConnected()){
this.encode(data, result => this.conn.send(result))
} else {
this.sendBuffer.push(() => this.encode(data, result => this.conn.send(result)))
}
}
/**
* Return the next message ref, accounting for overflows
* @returns {string}
*/
makeRef(){
let newRef = this.ref + 1
if(newRef === this.ref){ this.ref = 0 } else { this.ref = newRef }
return this.ref.toString()
}
sendHeartbeat(){
if(this.pendingHeartbeatRef && !this.isConnected()){ return }
this.pendingHeartbeatRef = this.makeRef()
this.push({topic: "phoenix", event: "heartbeat", payload: {}, ref: this.pendingHeartbeatRef})
this.heartbeatTimeoutTimer = setTimeout(() => this.heartbeatTimeout(), this.heartbeatIntervalMs)
}
flushSendBuffer(){
if(this.isConnected() && this.sendBuffer.length > 0){
this.sendBuffer.forEach(callback => callback())
this.sendBuffer = []
}
}
onConnMessage(rawMessage){
this.decode(rawMessage.data, msg => {
let {topic, event, payload, ref, join_ref} = msg
if(ref && ref === this.pendingHeartbeatRef){
this.clearHeartbeats()
this.pendingHeartbeatRef = null
this.heartbeatTimer = setTimeout(() => this.sendHeartbeat(), this.heartbeatIntervalMs)
}
if(this.hasLogger()) this.log("receive", `${payload.status || ""} ${topic} ${event} ${ref && "(" + ref + ")" || ""}`, payload)
for(let i = 0; i < this.channels.length; i++){
const channel = this.channels[i]
if(!channel.isMember(topic, event, payload, join_ref)){ continue }
channel.trigger(event, payload, ref, join_ref)
}
for(let i = 0; i < this.stateChangeCallbacks.message.length; i++){
let [, callback] = this.stateChangeCallbacks.message[i]
callback(msg)
}
})
}
leaveOpenTopic(topic){
let dupChannel = this.channels.find(c => c.topic === topic && (c.isJoined() || c.isJoining()))
if(dupChannel){
if(this.hasLogger()) this.log("transport", `leaving duplicate topic "${topic}"`)
dupChannel.leave()
}
}
}
================================================
FILE: assets/js/phoenix/timer.js
================================================
/**
*
* Creates a timer that accepts a `timerCalc` function to perform
* calculated timeout retries, such as exponential backoff.
*
* @example
* let reconnectTimer = new Timer(() => this.connect(), function(tries){
* return [1000, 5000, 10000][tries - 1] || 10000
* })
* reconnectTimer.scheduleTimeout() // fires after 1000
* reconnectTimer.scheduleTimeout() // fires after 5000
* reconnectTimer.reset()
* reconnectTimer.scheduleTimeout() // fires after 1000
*
* @param {Function} callback
* @param {Function} timerCalc
*/
export default class Timer {
constructor(callback, timerCalc){
this.callback = callback
this.timerCalc = timerCalc
this.timer = null
this.tries = 0
}
reset(){
this.tries = 0
clearTimeout(this.timer)
}
/**
* Cancels any previous scheduleTimeout and schedules callback
*/
scheduleTimeout(){
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.tries = this.tries + 1
this.callback()
}, this.timerCalc(this.tries + 1))
}
}
================================================
FILE: assets/js/phoenix/utils.js
================================================
// wraps value in closure or returns closure
export let closure = (value) => {
if(typeof value === "function"){
return value
} else {
let closure = function (){ return value }
return closure
}
}
================================================
FILE: assets/test/channel_test.js
================================================
import {jest} from "@jest/globals"
import {Channel, Socket} from "../js/phoenix"
let channel, socket
const defaultRef = 1
const defaultTimeout = 10000
class WSMock {
constructor(url, protocols){
this.url = url
this.protocols = protocols
}
close(){}
send(){}
}
describe("with transport", function (){
beforeAll(function (){
global.WebSocket = WSMock
})
afterAll(function (){
global.WebSocket = null
})
describe("constructor", function (){
beforeEach(function (){
socket = new Socket("/", {timeout: 1234})
})
it("sets defaults", function (){
channel = new Channel("topic", {one: "two"}, socket)
expect(channel.state).toBe("closed")
expect(channel.topic).toBe("topic")
expect(channel.params()).toEqual({one: "two"})
expect(channel.socket).toBe(socket)
expect(channel.timeout).toBe(1234)
expect(channel.joinedOnce).toBe(false)
expect(channel.joinPush).toBeTruthy()
expect(channel.pushBuffer).toEqual([])
})
it("sets up joinPush object with literal params", function (){
channel = new Channel("topic", {one: "two"}, socket)
const joinPush = channel.joinPush
expect(joinPush.channel).toBe(channel)
expect(joinPush.payload()).toEqual({one: "two"})
expect(joinPush.event).toBe("phx_join")
expect(joinPush.timeout).toBe(1234)
})
it("sets up joinPush object with closure params", function (){
channel = new Channel("topic", () => ({one: "two"}), socket)
const joinPush = channel.joinPush
expect(joinPush.channel).toBe(channel)
expect(joinPush.payload()).toEqual({one: "two"})
expect(joinPush.event).toBe("phx_join")
expect(joinPush.timeout).toBe(1234)
})
it("sets subprotocols when authToken is provided", function (){
const authToken = "1234"
const socket = new Socket("/socket", {authToken})
socket.connect()
expect(socket.conn.protocols).toEqual(["phoenix", "base64url.bearer.phx.MTIzNA"])
})
})
describe("updating join params", function (){
it("can update the join params", function (){
let counter = 0
let params = () => ({value: counter})
socket = {timeout: 1234, onError: function (){}, onOpen: function (){}}
channel = new Channel("topic", params, socket)
const joinPush = channel.joinPush
expect(joinPush.channel).toBe(channel)
expect(joinPush.payload()).toEqual({value: 0})
expect(joinPush.event).toBe("phx_join")
expect(joinPush.timeout).toBe(1234)
counter++
expect(joinPush.channel).toBe(channel)
expect(joinPush.payload()).toEqual({value: 1})
expect(channel.params()).toEqual({value: 1})
expect(joinPush.event).toBe("phx_join")
expect(joinPush.timeout).toBe(1234)
})
})
describe("join", function (){
beforeEach(function (){
socket = new Socket("/socket", {timeout: defaultTimeout})
channel = socket.channel("topic", {one: "two"})
})
it("sets state to joining", function (){
channel.join()
expect(channel.state).toBe("joining")
})
it("sets joinedOnce to true", function (){
expect(channel.joinedOnce).toBe(false)
channel.join()
expect(channel.joinedOnce).toBe(true)
})
it("throws if attempting to join multiple times", function (){
channel.join()
expect(() => channel.join()).toThrow(/^tried to join multiple times/)
})
it("triggers socket push with channel params", function (){
jest.spyOn(socket, "makeRef").mockReturnValue(defaultRef)
const spy = jest.spyOn(socket, "push")
channel.join()
expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith({
topic: "topic",
event: "phx_join",
payload: {one: "two"},
ref: defaultRef,
join_ref: channel.joinRef(),
})
})
it("can set timeout on joinPush", function (){
const newTimeout = 2000
const joinPush = channel.joinPush
expect(joinPush.timeout).toBe(defaultTimeout)
channel.join(newTimeout)
expect(joinPush.timeout).toBe(newTimeout)
})
it("leaves existing duplicate topic on new join", function (done){
channel.join().receive("ok", () => {
let newChannel = socket.channel("topic")
expect(channel.isJoined()).toBe(true)
newChannel.join()
expect(channel.isJoined()).toBe(false)
done()
})
channel.joinPush.trigger("ok", {})
})
describe("timeout behavior", function (){
let joinPush
const helpers = {
receiveSocketOpen(){
jest.spyOn(socket, "isConnected").mockReturnValue(true)
socket.onConnOpen()
},
}
beforeEach(function (){
jest.useFakeTimers()
joinPush = channel.joinPush
})
afterEach(function (){
jest.useRealTimers()
})
it("succeeds before timeout", function (){
const spy = jest.spyOn(socket, "push")
const timeout = joinPush.timeout
socket.connect()
helpers.receiveSocketOpen()
channel.join()
expect(spy).toHaveBeenCalledTimes(1)
expect(channel.timeout).toBe(10000)
jest.advanceTimersByTime(100)
joinPush.trigger("ok", {})
expect(channel.state).toBe("joined")
jest.advanceTimersByTime(timeout)
expect(spy).toHaveBeenCalledTimes(1)
})
it("retries with backoff after timeout", function (){
const spy = jest.spyOn(socket, "push")
const timeoutSpy = jest.fn()
const timeout = joinPush.timeout
socket.connect()
helpers.receiveSocketOpen()
channel.join().receive("timeout", timeoutSpy)
expect(spy).toHaveBeenCalledTimes(1)
expect(timeoutSpy).toHaveBeenCalledTimes(0)
jest.advanceTimersByTime(timeout)
expect(spy).toHaveBeenCalledTimes(2) // leave pushed to server
expect(timeoutSpy).toHaveBeenCalledTimes(1)
jest.advanceTimersByTime(timeout + 1000)
expect(spy).toHaveBeenCalledTimes(4) // leave + rejoin
expect(timeoutSpy).toHaveBeenCalledTimes(2)
jest.advanceTimersByTime(10000)
joinPush.trigger("ok", {})
expect(spy).toHaveBeenCalledTimes(6)
expect(channel.state).toBe("joined")
})
it("with socket and join delay", function (){
const spy = jest.spyOn(socket, "push")
jest.useFakeTimers()
const joinPush = channel.joinPush
channel.join()
expect(spy).toHaveBeenCalledTimes(1)
// open socket after delay
jest.advanceTimersByTime(9000)
expect(spy).toHaveBeenCalledTimes(1)
// join request returns between timeouts
jest.advanceTimersByTime(1000)
socket.connect()
expect(channel.state).toBe("errored")
helpers.receiveSocketOpen()
joinPush.trigger("ok", {})
// join request succeeds after delay
jest.advanceTimersByTime(1000)
expect(channel.state).toBe("joined")
expect(spy).toHaveBeenCalledTimes(3) // leave pushed to server
})
it("with socket delay only", function (){
jest.useFakeTimers()
const joinPush = channel.joinPush
channel.join()
expect(channel.state).toBe("joining")
// connect socket after delay
jest.advanceTimersByTime(6000)
socket.connect()
// open socket after delay
jest.advanceTimersByTime(5000)
helpers.receiveSocketOpen()
joinPush.trigger("ok", {})
joinPush.trigger("ok", {})
expect(channel.state).toBe("joined")
})
})
})
describe("joinPush", function (){
let joinPush
let response
const helpers = {
receiveOk(){
jest.advanceTimersByTime(joinPush.timeout / 2) // before timeout
return joinPush.channel.trigger("phx_reply", {status: "ok", response: response}, joinPush.ref, joinPush.ref)
// return joinPush.trigger("ok", response)
},
receiveTimeout(){
jest.advanceTimersByTime(joinPush.timeout * 2) // after timeout
},
receiveError(){
jest.advanceTimersByTime(joinPush.timeout / 2) // before timeout
return joinPush.trigger("error", response)
},
getBindings(event){
return channel.bindings.filter(bind => bind.event === event)
},
}
beforeEach(function (){
jest.useFakeTimers()
socket = new Socket("/socket", {timeout: defaultTimeout})
jest.spyOn(socket, "isConnected").mockReturnValue(true)
jest.spyOn(socket, "push").mockReturnValue(true)
channel = socket.channel("topic", {one: "two"})
joinPush = channel.joinPush
channel.join()
})
afterEach(function (){
jest.useRealTimers()
})
describe("receives 'ok'", function (){
beforeEach(function (){
response = {chan: "reply"}
})
it("sets channel state to joined", function (){
expect(channel.state).not.toBe("joined")
helpers.receiveOk()
expect(channel.state).toBe("joined")
})
it("triggers receive('ok') callback after ok response", function (){
const spyOk = jest.fn()
joinPush.receive("ok", spyOk)
helpers.receiveOk()
expect(spyOk).toHaveBeenCalledTimes(1)
})
it("triggers receive('ok') callback if ok response already received", function (){
const spyOk = jest.fn()
helpers.receiveOk()
joinPush.receive("ok", spyOk)
expect(spyOk).toHaveBeenCalledTimes(1)
})
it("does not trigger other receive callbacks after ok response", function (){
const spyError = jest.fn()
const spyTimeout = jest.fn()
joinPush.receive("error", spyError).receive("timeout", spyTimeout)
helpers.receiveOk()
jest.advanceTimersByTime(channel.timeout * 2) // attempt timeout
expect(spyError).not.toHaveBeenCalled()
expect(spyTimeout).not.toHaveBeenCalled()
})
it("clears timeoutTimer", function (){
expect(joinPush.timeoutTimer).toBeTruthy()
helpers.receiveOk()
expect(joinPush.timeoutTimer).toBeNull()
})
it("sets receivedResp", function (){
expect(joinPush.receivedResp).toBeNull()
helpers.receiveOk()
expect(joinPush.receivedResp).toEqual({status: "ok", response})
})
it("removes channel bindings", function (){
let bindings = helpers.getBindings("chan_reply_3")
expect(bindings.length).toBe(1)
helpers.receiveOk()
bindings = helpers.getBindings("chan_reply_3")
expect(bindings.length).toBe(0)
})
it("resets channel rejoinTimer", function (){
expect(channel.rejoinTimer).toBeTruthy()
const spy = jest.spyOn(channel.rejoinTimer, "reset")
helpers.receiveOk()
expect(spy).toHaveBeenCalledTimes(1)
})
it("sends and empties channel's buffered pushEvents", function (done){
const pushEvent = {send(){}}
const spy = jest.spyOn(pushEvent, "send")
channel.pushBuffer.push(pushEvent)
expect(channel.state).toBe("joining")
joinPush.receive("ok", () => {
expect(spy).toHaveBeenCalledTimes(1)
expect(channel.pushBuffer.length).toBe(0)
done()
})
helpers.receiveOk()
})
})
describe("receives 'timeout'", function (){
it("sets channel state to errored", function (done){
joinPush.receive("timeout", () => {
expect(channel.state).toBe("errored")
done()
})
helpers.receiveTimeout()
})
it("triggers receive('timeout') callback after ok response", function (){
const spyTimeout = jest.fn()
joinPush.receive("timeout", spyTimeout)
helpers.receiveTimeout()
expect(spyTimeout).toHaveBeenCalledTimes(1)
})
it("does not trigger other receive callbacks after timeout response", function (done){
const spyOk = jest.fn()
const spyError = jest.fn()
jest.spyOn(channel.rejoinTimer, "scheduleTimeout").mockReturnValue(true)
channel.test = true
joinPush.receive("ok", spyOk).receive("error", spyError).receive("timeout", () => {
expect(spyOk).not.toHaveBeenCalled()
expect(spyError).not.toHaveBeenCalled()
done()
})
helpers.receiveTimeout()
helpers.receiveOk()
})
it("schedules rejoinTimer timeout", function (){
expect(channel.rejoinTimer).toBeTruthy()
const spy = jest.spyOn(channel.rejoinTimer, "scheduleTimeout")
helpers.receiveTimeout()
expect(spy).toHaveBeenCalled() // TODO why called multiple times?
})
})
describe("receives 'error'", function (){
beforeEach(function (){
response = {chan: "fail"}
})
it("triggers receive('error') callback after error response", function (){
const spyError = jest.fn()
expect(channel.state).toBe("joining")
joinPush.receive("error", spyError)
helpers.receiveError()
joinPush.trigger("error", {})
expect(spyError).toHaveBeenCalledTimes(1)
})
it("triggers receive('error') callback if error response already received", function (){
const spyError = jest.fn()
helpers.receiveError()
joinPush.receive("error", spyError)
expect(spyError).toHaveBeenCalledTimes(1)
})
it("does not trigger other receive callbacks after error response", function (){
const spyOk = jest.fn()
const spyError = jest.fn()
const spyTimeout = jest.fn()
joinPush.receive("ok", spyOk).receive("error", () => {
spyError()
channel.leave()
}).receive("timeout", spyTimeout)
helpers.receiveError()
jest.advanceTimersByTime(channel.timeout * 2) // attempt timeout
expect(spyError).toHaveBeenCalledTimes(1)
expect(spyOk).not.toHaveBeenCalled()
expect(spyTimeout).not.toHaveBeenCalled()
})
it("clears timeoutTimer", function (){
expect(joinPush.timeoutTimer).toBeTruthy()
helpers.receiveError()
expect(joinPush.timeoutTimer).toBeNull()
})
it("sets receivedResp with error trigger after binding", function (done){
expect(joinPush.receivedResp).toBeNull()
joinPush.receive("error", resp => {
expect(resp).toEqual(response)
done()
})
helpers.receiveError()
})
it("sets receivedResp with error trigger before binding", function (done){
expect(joinPush.receivedResp).toBeNull()
helpers.receiveError()
joinPush.receive("error", resp => {
expect(resp).toEqual(response)
done()
})
})
it("does not set channel state to joined", function (){
helpers.receiveError()
expect(channel.state).toBe("errored")
})
it("does not trigger channel's buffered pushEvents", function (){
const pushEvent = {send: () => {}}
const spy = jest.spyOn(pushEvent, "send")
channel.pushBuffer.push(pushEvent)
helpers.receiveError()
expect(spy).not.toHaveBeenCalled()
expect(channel.pushBuffer.length).toBe(1)
})
})
})
describe("onError", function (){
let joinPush
beforeEach(function (){
jest.useFakeTimers()
socket = new Socket("/socket", {timeout: defaultTimeout})
jest.spyOn(socket, "isConnected").mockReturnValue(true)
jest.spyOn(socket, "push").mockReturnValue(true)
channel = socket.channel("topic", {one: "two"})
joinPush = channel.joinPush
channel.join()
joinPush.trigger("ok", {})
})
afterEach(function (){
jest.useRealTimers()
})
it("sets state to 'errored'", function (){
expect(channel.state).not.toBe("errored")
channel.trigger("phx_error")
expect(channel.state).toBe("errored")
})
it("does not trigger redundant errors during backoff", function (){
const spy = jest.spyOn(joinPush, "send").mockImplementation(() => {})
expect(spy).toHaveBeenCalledTimes(0)
channel.trigger("phx_error")
jest.advanceTimersByTime(1000)
expect(spy).toHaveBeenCalledTimes(1)
joinPush.trigger("error", {})
jest.advanceTimersByTime(10000)
expect(spy).toHaveBeenCalledTimes(1)
})
it("does not rejoin if channel leaving", function (){
channel.state = "leaving"
const spy = jest.spyOn(joinPush, "send")
socket.onConnError({})
jest.advanceTimersByTime(1000)
expect(spy).toHaveBeenCalledTimes(0)
jest.advanceTimersByTime(2000)
expect(spy).toHaveBeenCalledTimes(0)
expect(channel.state).toBe("leaving")
})
it("does not rejoin if channel closed", function (){
channel.state = "closed"
const spy = jest.spyOn(joinPush, "send")
socket.onConnError({})
jest.advanceTimersByTime(1000)
expect(spy).toHaveBeenCalledTimes(0)
jest.advanceTimersByTime(2000)
expect(spy).toHaveBeenCalledTimes(0)
expect(channel.state).toBe("closed")
})
it("triggers additional callbacks after join", function (){
const spy = jest.fn()
channel.onError(spy)
joinPush.trigger("ok", {})
expect(channel.state).toBe("joined")
expect(spy).toHaveBeenCalledTimes(0)
channel.trigger("phx_error")
expect(spy).toHaveBeenCalledTimes(1)
})
})
describe("onClose", function (){
let joinPush
beforeEach(function (){
jest.useFakeTimers()
socket = new Socket("/socket", {timeout: defaultTimeout})
jest.spyOn(socket, "isConnected").mockReturnValue(true)
jest.spyOn(socket, "push").mockReturnValue(true)
channel = socket.channel("topic", {one: "two"})
joinPush = channel.joinPush
channel.join()
})
afterEach(function (){
jest.useRealTimers()
})
it("sets state to 'closed'", function (){
expect(channel.state).not.toBe("closed")
channel.trigger("phx_close")
expect(channel.state).toBe("closed")
})
it("does not rejoin", function (){
const spy = jest.spyOn(joinPush, "send")
channel.trigger("phx_close")
jest.advanceTimersByTime(1000)
expect(spy).toHaveBeenCalledTimes(0)
jest.advanceTimersByTime(2000)
expect(spy).toHaveBeenCalledTimes(0)
})
it("triggers additional callbacks", function (){
const spy = jest.fn()
channel.onClose(spy)
expect(spy).toHaveBeenCalledTimes(0)
channel.trigger("phx_close")
expect(spy).toHaveBeenCalledTimes(1)
})
it("removes channel from socket", function (){
expect(socket.channels.length).toBe(1)
expect(socket.channels[0]).toBe(channel)
channel.trigger("phx_close")
expect(socket.channels.length).toBe(0)
})
})
describe("onMessage", function (){
it("returns payload by default", function (){
socket = new Socket("/socket")
channel = socket.channel("topic", {one: "two"})
jest.spyOn(socket, "makeRef").mockReturnValue(defaultRef)
const payload = channel.onMessage("event", {one: "two"}, defaultRef)
expect(payload).toEqual({one: "two"})
})
})
describe("canPush", function (){
beforeEach(function (){
socket = new Socket("/socket")
channel = socket.channel("topic", {one: "two"})
})
it("returns true when socket connected and channel joined", function (){
jest.spyOn(socket, "isConnected").mockReturnValue(true)
channel.state = "joined"
expect(channel.canPush()).toBe(true)
})
it("otherwise returns false", function (){
const isConnectedStub = jest.spyOn(socket, "isConnected")
isConnectedStub.mockReturnValue(false)
channel.state = "joined"
expect(channel.canPush()).toBe(false)
isConnectedStub.mockReturnValue(true)
channel.state = "joining"
expect(channel.canPush()).toBe(false)
isConnectedStub.mockReturnValue(false)
channel.state = "joining"
expect(channel.canPush()).toBe(false)
})
})
describe("on", function (){
beforeEach(function (){
socket = new Socket("/socket")
jest.spyOn(socket, "makeRef").mockReturnValue(defaultRef)
channel = socket.channel("topic", {one: "two"})
})
it("sets up callback for event", function (){
const spy = jest.fn()
channel.trigger("event", {}, defaultRef)
expect(spy).not.toHaveBeenCalled()
channel.on("event", spy)
channel.trigger("event", {}, defaultRef)
expect(spy).toHaveBeenCalled()
})
it("other event callbacks are ignored", function (){
const spy = jest.fn()
const ignoredSpy = jest.fn()
channel.trigger("event", {}, defaultRef)
expect(ignoredSpy).not.toHaveBeenCalled()
channel.on("event", spy)
channel.trigger("event", {}, defaultRef)
expect(ignoredSpy).not.toHaveBeenCalled()
})
it("generates unique refs for callbacks", function (){
const ref1 = channel.on("event1", () => 0)
const ref2 = channel.on("event2", () => 0)
expect(ref1 + 1).toBe(ref2)
})
it("calls all callbacks for event if they modified during event processing", function (){
const spy = jest.fn()
const ref = channel.on("event", () => {
channel.off("event", ref)
})
channel.on("event", spy)
channel.trigger("event", {}, defaultRef)
expect(spy).toHaveBeenCalled()
})
})
describe("off", function (){
beforeEach(function (){
socket = new Socket("/socket")
jest.spyOn(socket, "makeRef").mockReturnValue(defaultRef)
channel = socket.channel("topic", {one: "two"})
})
it("removes all callbacks for event", function (){
const spy1 = jest.fn()
const spy2 = jest.fn()
const spy3 = jest.fn()
channel.on("event", spy1)
channel.on("event", spy2)
channel.on("other", spy3)
channel.off("event")
channel.trigger("event", {}, defaultRef)
channel.trigger("other", {}, defaultRef)
expect(spy1).not.toHaveBeenCalled()
expect(spy2).not.toHaveBeenCalled()
expect(spy3).toHaveBeenCalled()
})
it("removes callback by its ref", function (){
const spy1 = jest.fn()
const spy2 = jest.fn()
const ref1 = channel.on("event", spy1)
const _ref2 = channel.on("event", spy2)
channel.off("event", ref1)
channel.trigger("event", {}, defaultRef)
expect(spy1).not.toHaveBeenCalled()
expect(spy2).toHaveBeenCalled()
})
})
describe("push", function (){
let joinPush
let socketSpy
const pushParams = (channel) => {
return {
topic: "topic",
event: "event",
payload: {foo: "bar"},
join_ref: channel.joinRef(),
ref: defaultRef,
}
}
beforeEach(function (){
jest.useFakeTimers()
socket = new Socket("/socket", {timeout: defaultTimeout})
jest.spyOn(socket, "makeRef").mockReturnValue(defaultRef)
jest.spyOn(socket, "isConnected").mockReturnValue(true)
socketSpy = jest.spyOn(socket, "push").mockReturnValue(undefined)
channel = socket.channel("topic", {one: "two"})
})
afterEach(function (){
jest.useRealTimers()
})
it("sends push event when successfully joined", function (){
channel.join().trigger("ok", {})
channel.push("event", {foo: "bar"})
expect(socketSpy).toHaveBeenCalledWith(pushParams(channel))
})
it("enqueues push event to be sent once join has succeeded", function (){
joinPush = channel.join()
channel.push("event", {foo: "bar"})
expect(socketSpy).not.toHaveBeenCalledWith(pushParams(channel))
jest.advanceTimersByTime(channel.timeout / 2)
joinPush.trigger("ok", {})
expect(socketSpy).toHaveBeenCalledWith(pushParams(channel))
})
it("does not push if channel join times out", function (){
joinPush = channel.join()
channel.push("event", {foo: "bar"})
expect(socketSpy).not.toHaveBeenCalledWith(pushParams(channel))
jest.advanceTimersByTime(channel.timeout * 2)
joinPush.trigger("ok", {})
expect(socketSpy).not.toHaveBeenCalledWith(pushParams(channel))
})
it("uses channel timeout by default", function (){
const timeoutSpy = jest.fn()
channel.join().trigger("ok", {})
channel.push("event", {foo: "bar"}).receive("timeout", timeoutSpy)
jest.advanceTimersByTime(channel.timeout / 2)
expect(timeoutSpy).not.toHaveBeenCalled()
jest.advanceTimersByTime(channel.timeout)
expect(timeoutSpy).toHaveBeenCalled()
})
it("accepts timeout arg", function (){
const timeoutSpy = jest.fn()
channel.join().trigger("ok", {})
channel.push("event", {foo: "bar"}, channel.timeout * 2).receive("timeout", timeoutSpy)
jest.advanceTimersByTime(channel.timeout)
expect(timeoutSpy).not.toHaveBeenCalled()
jest.advanceTimersByTime(channel.timeout * 2)
expect(timeoutSpy).toHaveBeenCalled()
})
it("does not time out after receiving 'ok'", function (){
channel.join().trigger("ok", {})
const timeoutSpy = jest.fn()
const push = channel.push("event", {foo: "bar"})
push.receive("timeout", timeoutSpy)
jest.advanceTimersByTime(push.timeout / 2)
expect(timeoutSpy).not.toHaveBeenCalled()
push.trigger("ok", {})
jest.advanceTimersByTime(push.timeout)
expect(timeoutSpy).not.toHaveBeenCalled()
})
it("throws if channel has not been joined", function (){
expect(() => channel.push("event", {})).toThrow(/^tried to push.*before joining/)
})
})
describe("leave", function (){
let socketSpy
beforeEach(function (){
jest.useFakeTimers()
socket = new Socket("/socket", {timeout: defaultTimeout})
jest.spyOn(socket, "isConnected").mockReturnValue(true)
socketSpy = jest.spyOn(socket, "push").mockReturnValue(undefined)
channel = socket.channel("topic", {one: "two"})
channel.join().trigger("ok", {})
})
afterEach(function (){
jest.useRealTimers()
})
it("unsubscribes from server events", function (){
jest.spyOn(socket, "makeRef").mockReturnValue(defaultRef)
const joinRef = channel.joinRef()
channel.leave()
expect(socketSpy).toHaveBeenCalledWith({
topic: "topic",
event: "phx_leave",
payload: {},
ref: defaultRef,
join_ref: joinRef,
})
})
it("closes channel on 'ok' from server", function (){
const anotherChannel = socket.channel("another", {three: "four"})
expect(socket.channels.length).toBe(2)
channel.leave().trigger("ok", {})
expect(socket.channels.length).toBe(1)
expect(socket.channels[0]).toBe(anotherChannel)
})
it("sets state to closed on 'ok' event", function (){
expect(channel.state).not.toBe("closed")
channel.leave().trigger("ok", {})
expect(channel.state).toBe("closed")
})
// TODO - the following tests are skipped until Channel.leave
// behavior can be fixed; currently, 'ok' is triggered immediately
// within Channel.leave so timeout callbacks are never reached
//
it.skip("sets state to leaving initially", function (){
expect(channel.state).not.toBe("leaving")
channel.leave()
expect(channel.state).toBe("leaving")
})
it.skip("closes channel on 'timeout'", function (){
channel.leave()
jest.advanceTimersByTime(channel.timeout)
expect(channel.state).toBe("closed")
})
it.skip("accepts timeout arg", function (){
channel.leave(channel.timeout * 2)
jest.advanceTimersByTime(channel.timeout)
expect(channel.state).toBe("leaving")
jest.advanceTimersByTime(channel.timeout * 2)
expect(channel.state).toBe("closed")
})
})
})
================================================
FILE: assets/test/longpoll_test.js
================================================
import {jest} from "@jest/globals"
import {LongPoll} from "../js/phoenix"
import {Socket} from "../js/phoenix"
import {AUTH_TOKEN_PREFIX} from "../js/phoenix/constants"
import Ajax from "../js/phoenix/ajax"
describe("LongPoll", () => {
let originalXHR
beforeEach(() => {
originalXHR = global.XMLHttpRequest
// Mock XMLHttpRequest
const mockOpen = jest.fn()
const mockSend = jest.fn()
const mockAbort = jest.fn()
const mockSetRequestHeader = jest.fn()
global.XMLHttpRequest = jest.fn(() => ({
open: mockOpen,
send: mockSend,
abort: mockAbort,
setRequestHeader: mockSetRequestHeader,
readyState: 4,
status: 200,
responseText: JSON.stringify({status: 200, token: "token123", messages: []}),
onreadystatechange: null,
}))
// Spy on Ajax.request
jest.spyOn(Ajax, "request").mockImplementation(() => {
return {abort: jest.fn()}
})
})
afterEach(() => {
global.XMLHttpRequest = originalXHR
jest.restoreAllMocks()
})
describe("constructor", () => {
it("should handle undefined protocols", () => {
const longpoll = new LongPoll("http://localhost/socket/longpoll", undefined)
// Verify longpoll was initialized correctly without error
expect(longpoll.pollEndpoint).toBe("http://localhost/socket/longpoll")
expect(longpoll.authToken).toBeUndefined()
expect(longpoll.readyState).toBe(0) // connecting
})
it("should handle null protocols", () => {
const longpoll = new LongPoll("http://localhost/socket/longpoll", null)
// Verify longpoll was initialized correctly without error
expect(longpoll.pollEndpoint).toBe("http://localhost/socket/longpoll")
expect(longpoll.authToken).toBeUndefined()
expect(longpoll.readyState).toBe(0) // connecting
})
it("should handle empty array protocols", () => {
const longpoll = new LongPoll("http://localhost/socket/longpoll", [])
// Verify longpoll was initialized correctly without error
expect(longpoll.pollEndpoint).toBe("http://localhost/socket/longpoll")
expect(longpoll.authToken).toBeUndefined()
expect(longpoll.readyState).toBe(0) // connecting
})
it("should extract authToken when valid protocols are provided", () => {
const authToken = "my-auth-token"
const encodedToken = btoa(authToken)
const protocols = ["phoenix", `${AUTH_TOKEN_PREFIX}${encodedToken}`]
const longpoll = new LongPoll("http://localhost/socket/longpoll", protocols)
// Verify auth token was extracted correctly
expect(longpoll.authToken).toBe(authToken)
})
})
describe("poll", () => {
it("should include auth token in headers when present", () => {
const authToken = "my-auth-token"
const encodedToken = btoa(authToken)
const protocols = ["phoenix", `${AUTH_TOKEN_PREFIX}${encodedToken}`]
const longpoll = new LongPoll("http://localhost/socket/longpoll", protocols)
longpoll.timeout = 1000
longpoll.poll()
// Verify Ajax.request was called with the correct headers
expect(Ajax.request).toHaveBeenCalledWith(
"GET",
expect.any(String),
{"Accept": "application/json", "X-Phoenix-AuthToken": authToken},
null,
expect.any(Number),
expect.any(Function),
expect.any(Function)
)
})
it("should not include auth token in headers when not present", () => {
const longpoll = new LongPoll("http://localhost/socket/longpoll", undefined)
longpoll.timeout = 1000
longpoll.poll()
// Verify Ajax.request was called without auth token header
expect(Ajax.request).toHaveBeenCalledWith(
"GET",
expect.any(String),
{"Accept": "application/json"},
null,
expect.any(Number),
expect.any(Function),
expect.any(Function)
)
})
it("should treat 410 as error when token already exists", () => {
const longpoll = new LongPoll("http://localhost/socket/longpoll", undefined)
longpoll.timeout = 1000
longpoll.token = "existing-token"
const mockOnerror = jest.fn()
const mockCloseAndRetry = jest.fn()
longpoll.onerror = mockOnerror
longpoll.closeAndRetry = mockCloseAndRetry
Ajax.request.mockImplementation((method, url, headers, body, timeout, ontimeout, callback) => {
callback({status: 410, token: "new-token", messages: []})
return {abort: jest.fn()}
})
longpoll.poll()
expect(mockOnerror).toHaveBeenCalledWith(410)
expect(mockCloseAndRetry).toHaveBeenCalledWith(3410, "session_gone", false)
})
})
describe("batchSend", () => {
it("should send with correct content-type header format", () => {
const longpoll = new LongPoll("http://localhost/socket/longpoll", undefined)
longpoll.timeout = 1000
const messages = ["message1", "message2"]
longpoll.batchSend(messages)
// Verify Ajax.request was called with correct headers format
expect(Ajax.request).toHaveBeenCalledWith(
"POST",
expect.any(String),
{"Content-Type": "application/x-ndjson"},
"message1\nmessage2",
expect.any(Number),
expect.any(Function),
expect.any(Function)
)
})
})
})
describe("Socket with LongPoll", () => {
describe("transportConnect", () => {
it("should initialize with undefined protocols when no auth token", () => {
const socket = new Socket("/socket", {transport: LongPoll})
// Mock the transport to capture the protocols argument
socket.transport = jest.fn(() => ({
onopen: jest.fn(),
onerror: jest.fn(),
onmessage: jest.fn(),
onclose: jest.fn()
}))
socket.transportConnect()
// Verify that the transport was called with undefined protocols
expect(socket.transport).toHaveBeenCalledWith(
expect.any(String),
undefined
)
})
it("should only set protocols array when auth token is present", () => {
const authToken = "my-auth-token"
const socket = new Socket("/socket", {
transport: LongPoll,
params: {token: authToken}
})
// Set auth token
socket.authToken = authToken
// Mock the transport to capture the protocols argument
socket.transport = jest.fn(() => ({
onopen: jest.fn(),
onerror: jest.fn(),
onmessage: jest.fn(),
onclose: jest.fn()
}))
socket.transportConnect()
// Verify that the transport was called with correct protocols array
expect(socket.transport).toHaveBeenCalledWith(
expect.any(String),
["phoenix", `${AUTH_TOKEN_PREFIX}${btoa(authToken).replace(/=/g, "")}`]
)
})
})
})
describe("Ajax.request", () => {
let originalXMLHttpRequest, originalFetch, originalAbortController
beforeEach(() => {
originalXMLHttpRequest = global.XMLHttpRequest
originalFetch = global.fetch
originalAbortController = global.AbortController
// Mock AbortController
global.AbortController = jest.fn(() => ({
abort: jest.fn(),
signal: {}
}))
// Mock XMLHttpRequest
global.XMLHttpRequest = jest.fn(() => ({
open: jest.fn(),
send: jest.fn(),
setRequestHeader: jest.fn(),
onreadystatechange: null,
readyState: 4,
status: 200,
responseText: JSON.stringify({success: true})
}))
// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
text: () => Promise.resolve(JSON.stringify({success: true}))
})
)
})
afterEach(() => {
global.XMLHttpRequest = originalXMLHttpRequest
global.fetch = originalFetch
global.AbortController = originalAbortController
jest.restoreAllMocks()
})
it("should use XMLHttpRequest by default", () => {
Ajax.request("GET", "/test-endpoint", {}, null, 0, null, (response) => {
expect(response).toEqual({success: true})
})
expect(global.XMLHttpRequest).toHaveBeenCalled()
})
it("should use fetch when XMLHttpRequest is not available", () => {
global.XMLHttpRequest = undefined // Simulate it being unavailable
Ajax.request("GET", "/test-endpoint", {}, null, 0, null, (response) => {
expect(response).toEqual({success: true})
})
expect(global.fetch).toHaveBeenCalledWith(
"/test-endpoint",
expect.objectContaining({
method: "GET",
})
)
})
})
================================================
FILE: assets/test/presence_test.js
================================================
import {Presence} from "../js/phoenix"
const clone = (obj) => {
let cloned = JSON.parse(JSON.stringify(obj))
Object.entries(obj).forEach(([key, val]) => {
if(val === undefined){
cloned[key] = undefined
}
})
return cloned
}
const fixtures = {
joins(){
return {u1: {metas: [{id: 1, phx_ref: "1.2"}]}}
},
leaves(){
return {u2: {metas: [{id: 2, phx_ref: "2"}]}}
},
state(){
return {
u1: {metas: [{id: 1, phx_ref: "1"}]},
u2: {metas: [{id: 2, phx_ref: "2"}]},
u3: {metas: [{id: 3, phx_ref: "3"}]},
}
},
}
const channelStub = {
ref: 1,
events: {},
on(event, callback){
this.events[event] = callback
},
trigger(event, data){
this.events[event](data)
},
joinRef(){
return `${this.ref}`
},
simulateDisconnectAndReconnect(){
this.ref++
},
}
const listByFirst = (id, {metas: [first, ..._rest]}) => first
describe("syncState", () => {
it("syncs empty state", () => {
let newState = {u1: {metas: [{id: 1, phx_ref: "1"}]}}
let state = {}
let stateBefore = clone(state)
Presence.syncState(state, newState)
expect(state).toEqual(stateBefore)
state = Presence.syncState(state, newState)
expect(state).toEqual(newState)
})
it("onJoins new presences and onLeave's left presences", () => {
let newState = fixtures.state()
let state = {u4: {metas: [{id: 4, phx_ref: "4"}]}}
let joined = {}
let left = {}
const onJoin = (key, current, newPres) => {
joined[key] = {current, newPres}
}
const onLeave = (key, current, leftPres) => {
left[key] = {current, leftPres}
}
state = Presence.syncState(state, newState, onJoin, onLeave)
expect(state).toEqual(newState)
expect(joined).toEqual({
u1: {current: undefined, newPres: {metas: [{id: 1, phx_ref: "1"}]}},
u2: {current: undefined, newPres: {metas: [{id: 2, phx_ref: "2"}]}},
u3: {current: undefined, newPres: {metas: [{id: 3, phx_ref: "3"}]}},
})
expect(left).toEqual({
u4: {current: {metas: []}, leftPres: {metas: [{id: 4, phx_ref: "4"}]}},
})
})
it("onJoins only newly added metas", () => {
let newState = {u3: {metas: [{id: 3, phx_ref: "3"}, {id: 3, phx_ref: "3.new"}]}}
let state = {u3: {metas: [{id: 3, phx_ref: "3"}]}}
let joined = []
let left = []
const onJoin = (key, current, newPres) => {
joined.push([key, clone({current, newPres})])
}
const onLeave = (key, current, leftPres) => {
left.push([key, clone({current, leftPres})])
}
state = Presence.syncState(state, clone(newState), onJoin, onLeave)
expect(state).toEqual(newState)
expect(joined).toEqual([
["u3", {current: {metas: [{id: 3, phx_ref: "3"}]}, newPres: {metas: [{id: 3, phx_ref: "3.new"}]}}],
])
expect(left).toEqual([])
})
})
describe("syncDiff", () => {
it("syncs empty state", () => {
let joins = {u1: {metas: [{id: 1, phx_ref: "1"}]}}
let state = Presence.syncDiff({}, {joins, leaves: {}})
expect(state).toEqual(joins)
})
it("removes presence when meta is empty and adds additional meta", () => {
let state = fixtures.state()
state = Presence.syncDiff(state, {joins: fixtures.joins(), leaves: fixtures.leaves()})
expect(state).toEqual({
u1: {metas: [{id: 1, phx_ref: "1"}, {id: 1, phx_ref: "1.2"}]},
u3: {metas: [{id: 3, phx_ref: "3"}]},
})
})
it("removes meta while leaving key if other metas exist", () => {
let state = {u1: {metas: [{id: 1, phx_ref: "1"}, {id: 1, phx_ref: "1.2"}]}}
state = Presence.syncDiff(state, {joins: {}, leaves: {u1: {metas: [{id: 1, phx_ref: "1"}]}}})
expect(state).toEqual({
u1: {metas: [{id: 1, phx_ref: "1.2"}]},
})
})
})
describe("list", () => {
it("lists full presence by default", () => {
let state = fixtures.state()
expect(Presence.list(state)).toEqual([
{metas: [{id: 1, phx_ref: "1"}]},
{metas: [{id: 2, phx_ref: "2"}]},
{metas: [{id: 3, phx_ref: "3"}]},
])
})
it("lists with custom function", () => {
let state = {u1: {metas: [{id: 1, phx_ref: "1.first"}, {id: 1, phx_ref: "1.second"}]}}
const listBy = (key, {metas: [first, ..._rest]}) => first
expect(Presence.list(state, listBy)).toEqual([{id: 1, phx_ref: "1.first"}])
})
})
describe("instance", () => {
it("syncs state and diffs", () => {
let presence = new Presence(channelStub)
let user1 = {metas: [{id: 1, phx_ref: "1"}]}
let user2 = {metas: [{id: 2, phx_ref: "2"}]}
let newState = {u1: user1, u2: user2}
channelStub.trigger("presence_state", newState)
expect(presence.list(listByFirst)).toEqual([{id: 1, phx_ref: "1"}, {id: 2, phx_ref: "2"}])
channelStub.trigger("presence_diff", {joins: {}, leaves: {u1: user1}})
expect(presence.list(listByFirst)).toEqual([{id: 2, phx_ref: "2"}])
})
it("applies pending diff if state is not yet synced", () => {
let presence = new Presence(channelStub)
let onJoins = []
let onLeaves = []
presence.onJoin((id, current, newPres) => {
onJoins.push(clone({id, current, newPres}))
})
presence.onLeave((id, current, leftPres) => {
onLeaves.push(clone({id, current, leftPres}))
})
let user1 = {metas: [{id: 1, phx_ref: "1"}]}
let user2 = {metas: [{id: 2, phx_ref: "2"}]}
let user3 = {metas: [{id: 3, phx_ref: "3"}]}
let newState = {u1: user1, u2: user2}
let leaves = {u2: user2}
channelStub.trigger("presence_diff", {joins: {}, leaves: leaves})
expect(presence.list(listByFirst)).toEqual([])
expect(presence.pendingDiffs).toEqual([{joins: {}, leaves: leaves}])
channelStub.trigger("presence_state", newState)
expect(onLeaves).toEqual([{id: "u2", current: {metas: []}, leftPres: {metas: [{id: 2, phx_ref: "2"}]}}])
expect(presence.list(listByFirst)).toEqual([{id: 1, phx_ref: "1"}])
expect(presence.pendingDiffs).toEqual([])
expect(onJoins).toEqual([
{id: "u1", current: undefined, newPres: {metas: [{id: 1, phx_ref: "1"}]}},
{id: "u2", current: undefined, newPres: {metas: [{id: 2, phx_ref: "2"}]}},
])
channelStub.simulateDisconnectAndReconnect()
expect(presence.inPendingSyncState()).toBe(true)
channelStub.trigger("presence_diff", {joins: {}, leaves: {u1: user1}})
expect(presence.list(listByFirst)).toEqual([{id: 1, phx_ref: "1"}])
channelStub.trigger("presence_state", {u1: user1, u3: user3})
expect(presence.list(listByFirst)).toEqual([{id: 3, phx_ref: "3"}])
})
it("allows custom channel events", () => {
let presence = new Presence(channelStub, {
events: {
state: "the_state",
diff: "the_diff",
},
})
let user1 = {metas: [{id: 1, phx_ref: "1"}]}
channelStub.trigger("the_state", {user1})
expect(presence.list(listByFirst)).toEqual([{id: 1, phx_ref: "1"}])
channelStub.trigger("the_diff", {joins: {}, leaves: {user1}})
expect(presence.list(listByFirst)).toEqual([])
})
it("updates existing meta for a presence update (leave + join)", () => {
let presence = new Presence(channelStub)
let onJoins = []
let onLeaves = []
let user1 = {metas: [{id: 1, phx_ref: "1"}]}
let user2 = {metas: [{id: 2, name: "chris", phx_ref: "2"}]}
let newState = {u1: user1, u2: user2}
channelStub.trigger("presence_state", clone(newState))
presence.onJoin((id, current, newPres) => {
onJoins.push(clone({id, current, newPres}))
})
presence.onLeave((id, current, leftPres) => {
onLeaves.push(clone({id, current, leftPres}))
})
expect(presence.list((id, {metas: metas}) => metas)).toEqual([
[{id: 1, phx_ref: "1"}],
[{id: 2, name: "chris", phx_ref: "2"}],
])
let leaves = {u2: user2}
let joins = {u2: {metas: [{id: 2, name: "chris.2", phx_ref: "2.2", phx_ref_prev: "2"}]}}
channelStub.trigger("presence_diff", {joins, leaves})
expect(presence.list((id, {metas: metas}) => metas)).toEqual([
[{id: 1, phx_ref: "1"}],
[{id: 2, name: "chris.2", phx_ref: "2.2", phx_ref_prev: "2"}],
])
expect(onJoins).toEqual([
{
id: "u2",
current: {metas: [{id: 2, name: "chris", phx_ref: "2"}]},
newPres: {metas: [{id: 2, name: "chris.2", phx_ref: "2.2", phx_ref_prev: "2"}]},
},
])
})
})
================================================
FILE: assets/test/serializer.js
================================================
export const encode = (msg) => {
let payload = [
msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload
]
return JSON.stringify(payload)
}
export const decode = (rawPayload) => {
let [join_ref, ref, topic, event, payload] = JSON.parse(rawPayload)
return {join_ref, ref, topic, event, payload}
}
================================================
FILE: assets/test/serializer_test.js
================================================
/**
* @jest-environment node
*/
import {TextEncoder, TextDecoder} from "util"
import {Serializer} from "../js/phoenix"
let exampleMsg = {join_ref: "0", ref: "1", topic: "t", event: "e", payload: {foo: 1}}
let binPayload = () => {
let buffer = new ArrayBuffer(1)
new DataView(buffer).setUint8(0, 1)
return buffer
}
describe("JSON", () => {
it("encodes general pushes", (done) => {
Serializer.encode(exampleMsg, (result) => {
expect(result).toBe("[\"0\",\"1\",\"t\",\"e\",{\"foo\":1}]")
done()
})
})
it("decodes", (done) => {
Serializer.decode("[\"0\",\"1\",\"t\",\"e\",{\"foo\":1}]", (result) => {
expect(result).toEqual(exampleMsg)
done()
})
})
})
describe("binary", () => {
it("encodes", (done) => {
let buffer = binPayload()
let bin = "\0\x01\x01\x01\x0101te\x01"
let decoder = new TextDecoder()
Serializer.encode({join_ref: "0", ref: "1", topic: "t", event: "e", payload: buffer}, (result) => {
expect(decoder.decode(result)).toBe(bin)
done()
})
})
it("encodes variable length segments", (done) => {
let buffer = binPayload()
let bin = "\0\x02\x01\x03\x02101topev\x01"
let decoder = new TextDecoder()
Serializer.encode({join_ref: "10", ref: "1", topic: "top", event: "ev", payload: buffer}, (result) => {
expect(decoder.decode(result)).toBe(bin)
done()
})
})
it("decodes push", (done) => {
let bin = "\0\x03\x03\n123topsome-event\x01\x01"
let buffer = new TextEncoder().encode(bin).buffer
let decoder = new TextDecoder()
Serializer.decode(buffer, (result) => {
expect(result.join_ref).toBe("123")
expect(result.ref).toBeNull()
expect(result.topic).toBe("top")
expect(result.event).toBe("some-event")
expect(result.payload.constructor).toBe(ArrayBuffer)
expect(decoder.decode(result.payload)).toBe("\x01\x01")
done()
})
})
it("decodes reply", (done) => {
let bin = "\x01\x03\x02\x03\x0210012topok\x01\x01"
let buffer = new TextEncoder().encode(bin).buffer
let decoder = new TextDecoder()
Serializer.decode(buffer, (result) => {
expect(result.join_ref).toBe("100")
expect(result.ref).toBe("12")
expect(result.topic).toBe("top")
expect(result.event).toBe("phx_reply")
expect(result.payload.status).toBe("ok")
expect(result.payload.response.constructor).toBe(ArrayBuffer)
expect(decoder.decode(result.payload.response)).toBe("\x01\x01")
done()
})
})
it("decodes broadcast", (done) => {
let bin = "\x02\x03\ntopsome-event\x01\x01"
let buffer = new TextEncoder().encode(bin).buffer
let decoder = new TextDecoder()
Serializer.decode(buffer, (result) => {
expect(result.join_ref).toBeNull()
expect(result.ref).toBeNull()
expect(result.topic).toBe("top")
expect(result.event).toBe("some-event")
expect(result.payload.constructor).toBe(ArrayBuffer)
expect(decoder.decode(result.payload)).toBe("\x01\x01")
done()
})
})
})
================================================
FILE: assets/test/socket_http_test.js
================================================
/**
* @jest-environment jsdom
* @jest-environment-options {"url": "http://example.com/"}
*/
import {Socket} from "../js/phoenix"
// sadly, jsdom can only be configured globally for a file
describe("protocol", function (){
it("returns ws when location.protocol is http", function (){
const socket = new Socket("/socket")
expect(socket.protocol()).toBe("ws")
})
})
describe("endpointURL", function (){
it("returns endpoint for given path on http host", function (){
const socket = new Socket("/socket")
expect(socket.endPointURL()).toBe(
"ws://example.com/socket/websocket?vsn=2.0.0",
)
})
})
================================================
FILE: assets/test/socket_test.js
================================================
import {jest} from "@jest/globals"
import {WebSocket, Server as WebSocketServer} from "mock-socket"
import {encode} from "./serializer"
import {Socket, LongPoll} from "../js/phoenix"
import {SOCKET_STATES} from "../js/phoenix/constants"
let socket
describe("with transports", function (){
beforeAll(() => {
window.WebSocket = WebSocket
const mockOpen = jest.fn()
const mockSend = jest.fn()
const mockAbort = jest.fn()
const mockSetRequestHeader = jest.fn()
global.XMLHttpRequest = jest.fn(() => ({
open: mockOpen,
send: mockSend,
abort: mockAbort,
setRequestHeader: mockSetRequestHeader,
readyState: 4,
status: 200,
responseText: JSON.stringify({}),
onreadystatechange: null,
}))
})
describe("constructor", function (){
it("sets defaults", function (){
socket = new Socket("/socket")
expect(socket.channels.length).toBe(0)
expect(socket.sendBuffer.length).toBe(0)
expect(socket.ref).toBe(0)
expect(socket.endPoint).toBe("/socket/websocket")
expect(socket.stateChangeCallbacks).toEqual({open: [], close: [], error: [], message: []})
expect(socket.transport).toBe(WebSocket)
expect(socket.timeout).toBe(10000)
expect(socket.longpollerTimeout).toBe(20000)
expect(socket.heartbeatIntervalMs).toBe(30000)
expect(socket.logger).toBeNull()
expect(socket.binaryType).toBe("arraybuffer")
expect(typeof socket.reconnectAfterMs).toBe("function")
})
it("supports closure or literal params", function (){
socket = new Socket("/socket", {params: {one: "two"}})
expect(socket.params()).toEqual({one: "two"})
socket = new Socket("/socket", {params: function (){ return ({three: "four"}) }})
expect(socket.params()).toEqual({three: "four"})
})
it("overrides some defaults with options", function (){
const customTransport = function transport(){ }
const customLogger = function logger(){ }
const customReconnect = function reconnect(){ }
socket = new Socket("/socket", {
timeout: 40000,
longpollerTimeout: 50000,
heartbeatIntervalMs: 60000,
transport: customTransport,
logger: customLogger,
reconnectAfterMs: customReconnect,
params: {one: "two"},
})
expect(socket.timeout).toBe(40000)
expect(socket.longpollerTimeout).toBe(50000)
expect(socket.heartbeatIntervalMs).toBe(60000)
expect(socket.transport).toBe(customTransport)
expect(socket.logger).toBe(customLogger)
expect(socket.params()).toEqual({one: "two"})
})
describe("with Websocket", function (){
it("defaults to Websocket transport if available", function (done){
let mockServer = new WebSocketServer("wss://example.com/")
socket = new Socket("/socket")
expect(socket.transport).toBe(WebSocket)
mockServer.stop(() => done())
})
})
describe("longPollFallbackMs", function (){
it("falls back to longpoll when set after primary transport failure", function (done){
let mockServer
socket = new Socket("/socket", {longPollFallbackMs: 20})
const replaceSpy = jest.spyOn(socket, "replaceTransport")
mockServer = new WebSocketServer("wss://example.test/")
mockServer.stop(() => {
expect(socket.transport).toBe(WebSocket)
socket.onError((_reason) => {
setTimeout(() => {
expect(replaceSpy).toHaveBeenCalledWith(LongPoll)
done()
}, 100)
})
socket.connect()
})
})
})
})
describe("visibilitychange", function (){
it("does not connect a socket that was never connected", function (){
socket = new Socket("/socket")
const teardownSpy = jest.spyOn(socket, "teardown")
Object.defineProperty(document, "visibilityState", {value: "hidden", writable: true})
window.dispatchEvent(new Event("visibilitychange"))
Object.defineProperty(document, "visibilityState", {value: "visible", writable: true})
window.dispatchEvent(new Event("visibilitychange"))
expect(teardownSpy).not.toHaveBeenCalled()
})
it("reconnects on visibility change after unclean close", function (){
socket = new Socket("/socket")
socket.closeWasClean = false
const teardownSpy = jest.spyOn(socket, "teardown")
Object.defineProperty(document, "visibilityState", {value: "visible", writable: true})
window.dispatchEvent(new Event("visibilitychange"))
expect(teardownSpy).toHaveBeenCalledTimes(1)
})
it("does not reconnect on visibility change after clean close", function (){
socket = new Socket("/socket")
socket.closeWasClean = true
const teardownSpy = jest.spyOn(socket, "teardown")
Object.defineProperty(document, "visibilityState", {value: "visible", writable: true})
window.dispatchEvent(new Event("visibilitychange"))
expect(teardownSpy).not.toHaveBeenCalled()
})
})
describe("protocol", function (){
beforeEach(function (){
socket = new Socket("/socket")
})
it("returns wss when location.protocol is https", function (){
expect(socket.protocol()).toBe("wss")
})
})
describe("endpointURL", function (){
it("returns endpoint for given full url", function (){
socket = new Socket("wss://example.org/chat")
expect(socket.endPointURL()).toBe("wss://example.org/chat/websocket?vsn=2.0.0")
})
it("returns endpoint for given protocol-relative url", function (){
socket = new Socket("//example.org/chat")
expect(socket.endPointURL()).toBe("wss://example.org/chat/websocket?vsn=2.0.0")
})
it("returns endpoint for given path on https host", function (){
socket = new Socket("/socket")
expect(socket.endPointURL()).toBe("wss://example.com/socket/websocket?vsn=2.0.0")
})
})
describe("connect with WebSocket", function (){
let mockServer
beforeAll(function (){
mockServer = new WebSocketServer("wss://example.com/")
})
afterAll(function (done){
mockServer.stop(() => done())
})
beforeEach(function (){
socket = new Socket("/socket")
})
it("establishes websocket connection with endpoint", function (){
socket.connect()
const conn = socket.conn
expect(conn instanceof WebSocket).toBeTruthy()
expect(conn.url).toBe(socket.endPointURL())
})
it("sets callbacks for connection", function (){
let opens = 0
socket.onOpen(() => ++opens)
let closes = 0
socket.onClose(() => ++closes)
let lastError
socket.onError((error) => lastError = error)
let lastMessage
socket.onMessage((message) => lastMessage = message.payload)
socket.connect()
socket.conn.onopen()
expect(opens).toBe(1)
socket.conn.onclose()
expect(closes).toBe(1)
socket.conn.onerror("error")
expect(lastError).toBe("error")
const data = {"topic": "topic", "event": "event", "payload": "payload", "status": "ok"}
socket.conn.onmessage({data: encode(data)})
expect(lastMessage).toBe("payload")
})
it("is idempotent", function (){
socket.connect()
const conn = socket.conn
socket.connect()
expect(conn).toBe(socket.conn)
})
})
describe("connect with long poll", function (){
beforeEach(function (){
socket = new Socket("/socket", {transport: LongPoll})
})
it("establishes long poll connection with endpoint", function (){
socket.connect()
const conn = socket.conn
expect(conn instanceof LongPoll).toBeTruthy()
expect(conn.pollEndpoint).toBe("https://example.com/socket/longpoll?vsn=2.0.0")
expect(conn.timeout).toBe(20000)
})
it("sets callbacks for connection", function (){
let opens = 0
socket.onOpen(() => ++opens)
let closes = 0
socket.onClose(() => ++closes)
let lastError
socket.onError((error) => lastError = error)
let lastMessage
socket.onMessage((message) => lastMessage = message.payload)
socket.connect()
socket.conn.onopen()
expect(opens).toBe(1)
socket.conn.onclose()
expect(closes).toBe(1)
socket.conn.onerror("error")
expect(lastError).toBe("error")
socket.connect()
const data = {"topic": "topic", "event": "event", "payload": "payload", "status": "ok"}
socket.conn.onmessage({data: encode(data)})
expect(lastMessage).toBe("payload")
})
it("is idempotent", function (){
socket.connect()
const conn = socket.conn
socket.connect()
expect(conn).toBe(socket.conn)
})
})
describe("disconnect", function (){
let mockServer
beforeAll(function (){
mockServer = new WebSocketServer("wss://example.com/")
})
afterAll(function (done){
mockServer.stop(() => done())
})
beforeEach(function (){
socket = new Socket("/socket")
})
it("removes existing connection", function (done){
socket.connect()
socket.disconnect()
socket.disconnect(() => {
expect(socket.conn).toBeNull()
done()
})
})
it("calls callback", function (done){
let count = 0
socket.connect()
socket.disconnect(() => {
count++
expect(count).toBe(1)
done()
})
})
it("calls connection close callback", function (done){
socket.connect()
const closeSpy = jest.spyOn(socket.conn, "close")
socket.disconnect(() => {
expect(closeSpy).toHaveBeenCalledWith(1000, "reason")
done()
}, 1000, "reason")
})
it("does not throw when no connection", function (){
expect(() => {
socket.disconnect()
}).not.toThrow()
})
it("properly tears down old connection when immediately reconnecting", function (){
const connections = []
const mockWebSocket = function StubWebSocketNoAutoClose(_url){
const conn = {
readyState: SOCKET_STATES.open,
get bufferedAmount(){ return 1 },
binaryType: "arraybuffer",
timeout: 20000,
onopen: null,
onerror: null,
onmessage: null,
onclose: null,
close(_code, _reason){
this.readyState = SOCKET_STATES.closing
setTimeout(() => {
this.readyState = SOCKET_STATES.closed
}, 1000)
},
send(){},
}
connections.push(conn)
return conn
}
jest.useFakeTimers()
socket = new Socket("/socket", {
heartbeatIntervalMs: 30000,
heartbeatTimeoutMs: 30000,
reconnectAfterMs: () => 10,
transport: mockWebSocket
})
socket.connect()
const originalConn = socket.conn
// Disconnect triggers teardown, which waits for bufferedAmount to be zero or 2250ms,
// then awaits SOCKET_STATES.closed before calling the callback.
const disconnected = jest.fn()
socket.disconnect(disconnected)
// For now, the conn is still set.
expect(socket.conn).toBeTruthy()
// Advance time by > 2250ms, which means we are waiting for socket to transition to closed
jest.advanceTimersByTime(3000)
// Now we call connect, while the teardown is still running
socket.connect()
// By now, waitForSocketClosed should be done, but now there's a new conn!
jest.advanceTimersByTime(3000)
expect(socket.conn).not.toBe(originalConn)
const openConns = connections.filter(c => c.readyState === SOCKET_STATES.open)
expect(openConns.length).toBe(1)
// Late teardown must not overwrite this.conn with null when it is already connB
expect(socket.conn).not.toBeNull()
// the original disconnected should have been called
expect(disconnected).toHaveBeenCalled()
jest.useRealTimers()
})
it("properly tears down old connection when disconnecting twice", function (){
const connections = []
const mockWebSocket = function StubWebSocketNoAutoClose(_url){
const conn = {
readyState: SOCKET_STATES.open,
get bufferedAmount(){ return 1 },
binaryType: "arraybuffer",
timeout: 20000,
onopen: null,
onerror: null,
onmessage: null,
onclose: null,
close(_code, _reason){
this.readyState = SOCKET_STATES.closing
setTimeout(() => {
this.readyState = SOCKET_STATES.closed
}, 1000)
},
send(){},
}
connections.push(conn)
return conn
}
jest.useFakeTimers()
socket = new Socket("/socket", {
heartbeatIntervalMs: 30000,
heartbeatTimeoutMs: 30000,
reconnectAfterMs: () => 10,
transport: mockWebSocket
})
socket.connect()
const disconnected = jest.fn()
socket.disconnect(disconnected)
// For now, the conn is still set.
expect(socket.conn).toBeTruthy()
// Advance time by > 2250ms, which means we are waiting for socket to transition to closed
jest.advanceTimersByTime(3000)
// Now we call disconnect again, while the teardown is still running
const disconnected2 = jest.fn()
socket.disconnect(disconnected2)
jest.advanceTimersByTime(10000)
const openConns = connections.filter(c => c.readyState === SOCKET_STATES.open)
expect(openConns.length).toBe(0)
expect(socket.conn).toBeNull()
// both disconnected functions should have been called
expect(disconnected).toHaveBeenCalled()
expect(disconnected2).toHaveBeenCalled()
jest.useRealTimers()
})
})
describe("connectionState", function (){
beforeEach(function (){
socket = new Socket("/socket")
})
it("defaults to closed", function (){
expect(socket.connectionState()).toBe("closed")
})
it("returns closed if readyState unrecognized", function (){
socket.connect()
socket.conn.readyState = 5678
expect(socket.connectionState()).toBe("closed")
})
it("returns connecting", function (){
socket.connect()
socket.conn.readyState = 0
expect(socket.connectionState()).toBe("connecting")
expect(socket.isConnected()).toBe(false)
})
it("returns open", function (){
socket.connect()
socket.conn.readyState = 1
expect(socket.connectionState()).toBe("open")
expect(socket.isConnected()).toBe(true)
})
it("returns closing", function (){
socket.connect()
socket.conn.readyState = 2
expect(socket.connectionState()).toBe("closing")
expect(socket.isConnected()).toBe(false)
})
it("returns closed", function (){
socket.connect()
socket.conn.readyState = 3
expect(socket.connectionState()).toBe("closed")
expect(socket.isConnected()).toBe(false)
})
})
describe("channel", function (){
let channel
beforeEach(function (){
socket = new Socket("/socket")
})
it("returns channel with given topic and params", function (){
channel = socket.channel("topic", {one: "two"})
expect(channel.socket).toBe(socket)
expect(channel.topic).toBe("topic")
expect(channel.params()).toEqual({one: "two"})
})
it("adds channel to sockets channels list", function (){
expect(socket.channels.length).toBe(0)
channel = socket.channel("topic", {one: "two"})
expect(socket.channels.length).toBe(1)
const [foundChannel] = socket.channels
expect(foundChannel).toBe(channel)
})
})
describe("remove", function (){
it("removes given channel from channels", function (){
socket = new Socket("/socket")
const channel1 = socket.channel("topic-1")
const channel2 = socket.channel("topic-2")
jest.spyOn(channel1, "joinRef").mockReturnValue(1)
jest.spyOn(channel2, "joinRef").mockReturnValue(2)
expect(socket.stateChangeCallbacks.open.length).toBe(2)
socket.remove(channel1)
expect(socket.stateChangeCallbacks.open.length).toBe(1)
expect(socket.channels.length).toBe(1)
const [foundChannel] = socket.channels
expect(foundChannel).toBe(channel2)
})
})
describe("push", function (){
let data, json
beforeEach(function (){
data = {topic: "topic", event: "event", payload: "payload", ref: "ref"}
json = encode(data)
socket = new Socket("/socket")
})
it("sends data to connection when connected", function (){
socket.connect()
socket.conn.readyState = 1 // open
const sendSpy = jest.spyOn(socket.conn, "send")
socket.push(data)
expect(sendSpy).toHaveBeenCalledWith(json)
})
it("buffers data when not connected", function (){
socket.connect()
socket.conn.readyState = 0 // connecting
const sendSpy = jest.spyOn(socket.conn, "send").mockImplementation(() => {})
expect(socket.sendBuffer.length).toBe(0)
socket.push(data)
expect(sendSpy).not.toHaveBeenCalledWith(json)
expect(socket.sendBuffer.length).toBe(1)
const [callback] = socket.sendBuffer
callback()
expect(sendSpy).toHaveBeenCalledWith(json)
})
})
describe("makeRef", function (){
beforeEach(function (){
socket = new Socket("/socket")
})
it("returns next message ref", function (){
expect(socket.ref).toBe(0)
expect(socket.makeRef()).toBe("1")
expect(socket.ref).toBe(1)
expect(socket.makeRef()).toBe("2")
expect(socket.ref).toBe(2)
})
it("restarts for overflow", function (){
socket.ref = Number.MAX_SAFE_INTEGER + 1
expect(socket.makeRef()).toBe("0")
expect(socket.ref).toBe(0)
})
})
describe("sendHeartbeat", function (){
beforeEach(function (){
socket = new Socket("/socket")
socket.connect()
})
it("closes socket when heartbeat is not ack'd within heartbeat window", function (done){
jest.useFakeTimers()
let closed = false
socket.conn.readyState = 1 // open
socket.conn.close = () => closed = true
socket.sendHeartbeat()
expect(closed).toBe(false)
jest.advanceTimersByTime(10000)
expect(closed).toBe(false)
jest.advanceTimersByTime(20010)
expect(closed).toBe(true)
jest.useRealTimers()
done()
})
it("pushes heartbeat data when connected", function (){
socket.conn.readyState = 1 // open
const sendSpy = jest.spyOn(socket.conn, "send")
const data = "[null,\"1\",\"phoenix\",\"heartbeat\",{}]"
socket.sendHeartbeat()
expect(sendSpy).toHaveBeenCalledWith(data)
})
it("no ops when not connected", function (){
socket.conn.readyState = 0 // connecting
const sendSpy = jest.spyOn(socket.conn, "send")
const data = encode({topic: "phoenix", event: "heartbeat", payload: {}, ref: "1"})
socket.sendHeartbeat()
expect(sendSpy).not.toHaveBeenCalledWith(data)
})
})
describe("flushSendBuffer", function (){
beforeEach(function (){
socket = new Socket("/socket")
socket.connect()
})
it("calls callbacks in buffer when connected", function (){
socket.conn.readyState = 1 // open
const spy1 = jest.fn()
const spy2 = jest.fn()
socket.sendBuffer.push(spy1)
socket.sendBuffer.push(spy2)
socket.flushSendBuffer()
expect(spy1).toHaveBeenCalledTimes(1)
expect(spy2).toHaveBeenCalledTimes(1)
})
it("empties sendBuffer", function (){
socket.conn.readyState = 1 // open
socket.sendBuffer.push(() => { })
socket.flushSendBuffer()
expect(socket.sendBuffer.length).toBe(0)
})
})
describe("onConnOpen", function (){
let mockServer
beforeAll(function (){
mockServer = new WebSocketServer("wss://example.com/")
})
afterAll(function (done){
mockServer.stop(() => done())
})
beforeEach(function (){
socket = new Socket("/socket", {
reconnectAfterMs: () => 100000
})
socket.connect()
})
it("flushes the send buffer", function (){
socket.conn.readyState = 1 // open
const spy = jest.fn()
socket.sendBuffer.push(spy)
socket.onConnOpen()
expect(spy).toHaveBeenCalledTimes(1)
})
it("resets reconnectTimer", function (){
const resetSpy = jest.spyOn(socket.reconnectTimer, "reset")
socket.onConnOpen()
expect(resetSpy).toHaveBeenCalledTimes(1)
})
it("triggers onOpen callback", function (){
const spy = jest.fn()
socket.onOpen(spy)
socket.onConnOpen()
expect(spy).toHaveBeenCalledTimes(1)
})
})
describe("onConnClose", function (){
let mockServer
beforeAll(function (){
mockServer = new WebSocketServer("wss://example.com/")
})
afterAll(function (done){
mockServer.stop(() => done())
})
beforeEach(function (){
socket = new Socket("/socket", {
reconnectAfterMs: () => 100000
})
socket.connect()
})
it("does not schedule reconnectTimer if normal close", function (){
const scheduleSpy = jest.spyOn(socket.reconnectTimer, "scheduleTimeout")
const event = {code: 1000}
socket.onConnClose(event)
expect(scheduleSpy).not.toHaveBeenCalled()
})
it("schedules reconnectTimer timeout if abnormal close", function (){
const scheduleSpy = jest.spyOn(socket.reconnectTimer, "scheduleTimeout")
const event = {code: 1006}
socket.onConnClose(event)
expect(scheduleSpy).toHaveBeenCalledTimes(1)
})
it("does not schedule reconnectTimer timeout if normal close after explicit disconnect", function (){
const scheduleSpy = jest.spyOn(socket.reconnectTimer, "scheduleTimeout")
socket.disconnect()
expect(scheduleSpy).not.toHaveBeenCalled()
})
it("schedules reconnectTimer timeout if not normal close", function (){
const scheduleSpy = jest.spyOn(socket.reconnectTimer, "scheduleTimeout")
const event = {code: 1001}
socket.onConnClose(event)
expect(scheduleSpy).toHaveBeenCalledTimes(1)
})
it("schedules reconnectTimer timeout if connection cannot be made after a previous clean disconnect", function (done){
const scheduleSpy = jest.spyOn(socket.reconnectTimer, "scheduleTimeout")
socket.disconnect(() => {
socket.connect()
const event = {code: 1001}
socket.onConnClose(event)
expect(scheduleSpy).toHaveBeenCalledTimes(1)
done()
})
})
it("triggers onClose callback", function (){
const spy = jest.fn()
socket.onClose(spy)
socket.onConnClose("event")
expect(spy).toHaveBeenCalledWith("event")
})
it("triggers channel error if joining", function (){
const channel = socket.channel("topic")
const triggerSpy = jest.spyOn(channel, "trigger")
channel.join()
expect(channel.state).toBe("joining")
socket.onConnClose()
expect(triggerSpy).toHaveBeenCalledWith("phx_error")
})
it("triggers channel error if joined", function (){
const channel = socket.channel("topic")
const triggerSpy = jest.spyOn(channel, "trigger")
channel.join().trigger("ok", {})
expect(channel.state).toBe("joined")
socket.onConnClose()
expect(triggerSpy).toHaveBeenCalledWith("phx_error")
})
it("does not trigger channel error after leave", function (){
const channel = socket.channel("topic")
const triggerSpy = jest.spyOn(channel, "trigger")
channel.join().trigger("ok", {})
channel.leave()
expect(channel.state).toBe("closed")
socket.onConnClose()
expect(triggerSpy).not.toHaveBeenCalledWith("phx_error")
})
it("does not send heartbeat after explicit disconnect", function (done){
jest.useFakeTimers()
const sendHeartbeatSpy = jest.spyOn(socket, "sendHeartbeat")
socket.onConnOpen()
socket.disconnect()
jest.advanceTimersByTime(30000)
expect(sendHeartbeatSpy).not.toHaveBeenCalled()
jest.useRealTimers()
done()
})
it("does not timeout the heartbeat after explicit disconnect", function (done){
jest.useFakeTimers()
const heartbeatTimeoutSpy = jest.spyOn(socket, "heartbeatTimeout")
socket.onConnOpen()
socket.disconnect()
jest.advanceTimersByTime(60000)
expect(heartbeatTimeoutSpy).not.toHaveBeenCalled()
jest.useRealTimers()
done()
})
})
describe("onConnError", function (){
let mockServer
beforeAll(function (){
mockServer = new WebSocketServer("wss://example.com/")
})
afterAll(function (done){
mockServer.stop(() => done())
})
beforeEach(function (){
socket = new Socket("/socket", {
reconnectAfterMs: () => 100000
})
socket.connect()
})
it("triggers onClose callback", function (){
const spy = jest.fn()
socket.onError(spy)
socket.onConnError("error")
expect(spy).toHaveBeenCalledWith("error", expect.any(Function), 0)
})
it("triggers channel error if joining with open connection", function (){
const channel = socket.channel("topic")
const triggerSpy = jest.spyOn(channel, "trigger")
channel.join()
socket.onConnOpen()
expect(channel.state).toBe("joining")
socket.onConnError("error")
expect(triggerSpy).toHaveBeenCalledWith("phx_error")
})
it("triggers channel error if joining with no connection", function (){
const channel = socket.channel("topic")
const triggerSpy = jest.spyOn(channel, "trigger")
channel.join()
expect(channel.state).toBe("joining")
socket.onConnError("error")
expect(triggerSpy).toHaveBeenCalledWith("phx_error")
})
it("triggers channel error if joined", function (){
const channel = socket.channel("topic")
const triggerSpy = jest.spyOn(channel, "trigger")
channel.join().trigger("ok", {})
socket.onConnOpen()
expect(channel.state).toBe("joined")
let connectionsCount = null
let transport = null
socket.onError((error, erroredTransport, conns) => {
transport = erroredTransport
connectionsCount = conns
})
socket.onConnError("error")
expect(transport).toBe(WebSocket)
expect(connectionsCount).toBe(1)
expect(triggerSpy).toHaveBeenCalledWith("phx_error")
})
it("does not trigger channel error after leave", function (){
const channel = socket.channel("topic")
const triggerSpy = jest.spyOn(channel, "trigger")
channel.join().trigger("ok", {})
channel.leave()
expect(channel.state).toBe("closed")
socket.onConnError("error")
expect(triggerSpy).not.toHaveBeenCalledWith("phx_error")
})
it("does not trigger channel error if transport replaced with no previous connection", function (){
const channel = socket.channel("topic")
const triggerSpy = jest.spyOn(channel, "trigger")
channel.join()
expect(channel.state).toBe("joining")
let connectionsCount = null
class FakeTransport { }
socket.onError((error, transport, conns) => {
socket.replaceTransport(FakeTransport)
connectionsCount = conns
})
socket.onConnError("error")
expect(connectionsCount).toBe(0)
expect(socket.transport).toBe(FakeTransport)
expect(triggerSpy).not.toHaveBeenCalledWith("phx_error")
})
})
describe("onConnMessage", function (){
let mockServer
beforeAll(function (){
mockServer = new WebSocketServer("wss://example.com/")
})
afterAll(function (done){
mockServer.stop(() => done())
})
beforeEach(function (){
socket = new Socket("/socket", {
reconnectAfterMs: () => 100000
})
socket.connect()
})
it("parses raw message and triggers channel event", function (){
const message = encode({topic: "topic", event: "event", payload: "payload", ref: "ref"})
const data = {data: message}
const targetChannel = socket.channel("topic")
const otherChannel = socket.channel("off-topic")
const targetSpy = jest.spyOn(targetChannel, "trigger")
const otherSpy = jest.spyOn(otherChannel, "trigger")
socket.onConnMessage(data)
expect(targetSpy).toHaveBeenCalledWith("event", "payload", "ref", null)
expect(targetSpy).toHaveBeenCalledTimes(1)
expect(otherSpy).toHaveBeenCalledTimes(0)
})
it("triggers onMessage callback", function (){
const message = {"topic": "topic", "event": "event", "payload": "payload", "ref": "ref"}
const spy = jest.fn()
socket.onMessage(spy)
socket.onConnMessage({data: encode(message)})
expect(spy).toHaveBeenCalledWith({
"topic": "topic",
"event": "event",
"payload": "payload",
"ref": "ref",
"join_ref": null
})
})
})
describe("ping", function (){
beforeEach(function (){
socket = new Socket("/socket")
socket.connect()
})
it("pushes when connected", function (done){
let latency = 100
socket.conn.readyState = 1 // open
expect(socket.isConnected()).toBe(true)
socket.push = (msg) => {
setTimeout(() => {
socket.onConnMessage({data: encode({topic: "phoenix", event: "phx_reply", ref: msg.ref})})
}, latency)
}
const result = socket.ping(rtt => {
// if we're unlucky we could also receive 99 as rtt, so let's be generous
expect(rtt >= (latency - 10)).toBe(true)
done()
})
expect(result).toBe(true)
})
it("returns false when disconnected", function (){
socket.conn.readyState = 0
expect(socket.isConnected()).toBe(false)
const result = socket.ping(_rtt => true)
expect(result).toBe(false)
})
})
describe("custom encoder and decoder", function (){
it("encodes to JSON array by default", function (){
socket = new Socket("/socket")
const payload = {topic: "topic", ref: "2", join_ref: "1", event: "join", payload: {foo: "bar"}}
socket.encode(payload, encoded => {
expect(encoded).toBe("[\"1\",\"2\",\"topic\",\"join\",{\"foo\":\"bar\"}]")
})
})
it("allows custom encoding when using WebSocket transport", function (){
const encoder = (payload, callback) => callback("encode works")
socket = new Socket("/socket", {transport: WebSocket, encode: encoder})
socket.encode({foo: "bar"}, encoded => {
expect(encoded).toBe("encode works")
})
})
it("forces JSON encoding when using LongPoll transport", function (){
const encoder = (payload, callback) => callback("encode works")
socket = new Socket("/socket", {transport: LongPoll, encode: encoder})
const payload = {topic: "topic", ref: "2", join_ref: "1", event: "join", payload: {foo: "bar"}}
socket.encode(payload, encoded => {
expect(encoded).toBe("[\"1\",\"2\",\"topic\",\"join\",{\"foo\":\"bar\"}]")
})
})
it("decodes JSON by default", function (){
socket = new Socket("/socket")
const encoded = "[\"1\",\"2\",\"topic\",\"join\",{\"foo\":\"bar\"}]"
socket.decode(encoded, decoded => {
expect(decoded).toEqual({topic: "topic", ref: "2", join_ref: "1", event: "join", payload: {foo: "bar"}})
})
})
it("allows custom decoding when using WebSocket transport", function (){
const decoder = (payload, callback) => callback("decode works")
socket = new Socket("/socket", {transport: WebSocket, decode: decoder})
socket.decode("...esoteric format...", decoded => {
expect(decoded).toBe("decode works")
})
})
it("forces JSON decoding when using LongPoll transport", function (){
const decoder = (payload, callback) => callback("decode works")
socket = new Socket("/socket", {transport: LongPoll, decode: decoder})
const payload = {topic: "topic", ref: "2", join_ref: "1", event: "join", payload: {foo: "bar"}}
socket.decode("[\"1\",\"2\",\"topic\",\"join\",{\"foo\":\"bar\"}]", decoded => {
expect(decoded).toEqual(payload)
})
})
})
})
window.XMLHttpRequest = jest.fn()
window.WebSocket = WebSocket
================================================
FILE: babel.config.json
================================================
{
"presets": [
"@babel/preset-env"
]
}
================================================
FILE: config/config.exs
================================================
import Config
config :logger, :console,
colors: [enabled: false],
format: "\n$time $metadata[$level] $message\n"
config :phoenix,
json_library: Jason,
stacktrace_depth: 20,
trim_on_html_eex_engine: false,
sort_verified_routes_query_params: true
if Mix.env() == :dev do
esbuild = fn args ->
[
args: ~w(./js/phoenix --bundle) ++ args,
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
end
config :esbuild,
version: "0.25.4",
module: esbuild.(~w(--format=esm --sourcemap --outfile=../priv/static/phoenix.mjs)),
main: esbuild.(~w(--format=cjs --sourcemap --outfile=../priv/static/phoenix.cjs.js)),
cdn:
esbuild.(
~w(--target=es2016 --format=iife --global-name=Phoenix --outfile=../priv/static/phoenix.js)
),
cdn_min:
esbuild.(
~w(--target=es2016 --format=iife --global-name=Phoenix --minify --outfile=../priv/static/phoenix.min.js)
)
end
================================================
FILE: eslint.config.mjs
================================================
import jest from "eslint-plugin-jest"
import js from "@eslint/js"
import stylistic from "@stylistic/eslint-plugin"
export default [
{
// eslint config is very unintuitive; they will match an js file in any
// directory by default and you can only expand this;
// moreover, to have a global ignore, it must be specified without
// any other key as a separate object...
ignores: [
"integration_test/",
"installer/",
"doc/",
"deps/",
"coverage/",
"priv/",
"tmp/",
"test/"
],
},
{
...js.configs.recommended,
plugins: {
jest,
"@stylistic": stylistic
},
languageOptions: {
globals: {
...jest.environments.globals.globals,
global: "writable",
},
ecmaVersion: 12,
sourceType: "module",
},
rules: {
"@stylistic/indent": ["error", 2, {
SwitchCase: 1,
}],
"@stylistic/linebreak-style": ["error", "unix"],
"@stylistic/quotes": ["error", "double"],
"@stylistic/semi": ["error", "never"],
"@stylistic/object-curly-spacing": ["error", "never", {
objectsInObjects: false,
arraysInObjects: false,
}],
"@stylistic/array-bracket-spacing": ["error", "never"],
"@stylistic/comma-spacing": ["error", {
before: false,
after: true,
}],
"@stylistic/computed-property-spacing": ["error", "never"],
"@stylistic/space-before-blocks": ["error", {
functions: "never",
keywords: "never",
classes: "always",
}],
"@stylistic/keyword-spacing": ["error", {
overrides: {
if: {
after: false,
},
for: {
after: false,
},
while: {
after: false,
},
switch: {
after: false,
},
},
}],
"@stylistic/eol-last": ["error", "always"],
"no-unused-vars": ["error", {
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
}],
"no-useless-escape": "off",
"no-cond-assign": "off",
"no-case-declarations": "off",
},
}]
================================================
FILE: guides/asset_management.md
================================================
# Asset Management
Beside producing HTML, most web applications have various assets (JavaScript, CSS, images, fonts and so on).
From Phoenix v1.7, new applications use [esbuild](https://esbuild.github.io/) to prepare assets via the [Elixir esbuild wrapper](https://github.com/phoenixframework/esbuild), and [tailwindcss](https://tailwindcss.com) via the [Elixir tailwindcss wrapper](https://github.com/phoenixframework/tailwind) for CSS. The direct integration with `esbuild` and `tailwind` means that newly generated applications do not have dependencies on Node.js or an external build system (e.g. Webpack).
Your JavaScript is typically placed at "assets/js/app.js" and `esbuild` will extract it to "priv/static/assets/js/app.js". In development, this is done automatically via the `esbuild` watcher. In production, this is done by running `mix assets.deploy`.
`esbuild` can also handle your CSS files, but by default `tailwind` handles all CSS building.
Finally, all other assets, that usually don't have to be preprocessed, go directly to "priv/static".
## Third-party JS packages
If you want to import JavaScript dependencies, you have at least three options to add them to your application:
1. Vendor those dependencies inside your project and import them in your "assets/js/app.js" using a relative path:
```javascript
import topbar from "../vendor/topbar"
```
2. Call `npm install topbar --prefix assets`, which will create `package.json` and `package-lock.json` inside your assets directory, and `esbuild` will be able to automatically pick them up:
```javascript
import topbar from "topbar"
```
To ensure that `npm install` is being run when checking out your project, or when building a release, add a `"cmd --cd assets npm ci"` step in `mix.exs` to the `assets.deploy` and `assets.build` steps:
```elixir
"assets.build": ["cmd --cd assets npm ci", "tailwind your_app", "esbuild your_app"],
"assets.deploy": [
"cmd --cd assets npm ci",
"tailwind your_app --minify",
"esbuild your_app --minify",
"phx.digest"
]
```
3. Use Mix to track the dependency from a source repository:
```elixir
# mix.exs
{:topbar, github: "buunguyen/topbar", app: false, compile: false}
```
Run `mix deps.get` to fetch the dependency and then import it:
```javascript
import topbar from "topbar"
```
New applications use this third approach to import icons, such as Heroicons,
to avoid vendoring a copy of all icons and to avoid additional system
dependencies such as `npm`, while you can still track explicit versions
thanks to Mix. It is important to note that git dependencies cannot be used
by Hex packages, so if you intend to publish your project to Hex, consider
alternatives approaches.
Note that if you use third party JS package managers, you might need to adjust your
deployment steps to properly include the packages. If you're using
`mix phx.gen.release --docker`, have a look at the
[documentation](Mix.Tasks.Phx.Gen.Release.html#module-docker) for further details.
## Images, fonts, and external files
If you reference an external file in your CSS or JavaScript files, `esbuild` will attempt to validate and manage them, unless told otherwise.
For example, imagine you want to reference `priv/static/images/bg.png`, served at `/images/bg.png`, from your CSS file:
```css
body {
background-image: url(/images/bg.png);
}
```
The above may fail with the following message:
```text
error: Could not resolve "/images/bg.png" (mark it as external to exclude it from the bundle)
```
Given the images are already managed by Phoenix, you need to mark all resources from `/images` (and also `/fonts`) as external, as the error message says. This is what Phoenix does by default for new apps since v1.6.1+. In your `config/config.exs`, you will find:
```elixir
args: ~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets/js --external:/fonts/* --external:/images/*),
```
If you need to reference other directories, you need to update the arguments above accordingly. Note running `mix phx.digest` will create digested files for all of the assets in `priv/static`, so your images and fonts are still cache-busted.
### Ensuring fonts and images from third-party libraries are loaded
If you import a Node package that depends on additional fonts or images, you might find them to fail to load. This is because they are referenced in the JS or CSS but by default Esbuild will not touch or process referenced files. You can add arguments to esbuild in `config/config.exs` to ensure that the referenced resources are copied to the output folder. The following example would copy all referenced font files to the output folder and prefix the paths with `/assets/`:
```elixir
args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/* --public-path=/assets/ --loader:.woff=copy --loader:.ttf=copy --loader:.eot=copy --loader:.woff2=copy),
```
For more information, see [the esbuild documentation](https://esbuild.github.io/content-types/#copy).
## Esbuild plugins
Phoenix's default configuration of `esbuild` (via the Elixir wrapper) does not allow you to use [esbuild plugins](https://esbuild.github.io/plugins/). If you want to use an esbuild plugin, for example to compile SASS files to CSS, you can replace the default build system with a custom build script.
The following is an example of a custom build using esbuild via Node.js. First of all, you'll need to install Node.js in development and make it available for your production build step.
Then you'll need to add `esbuild` to your Node.js packages and the Phoenix packages. Inside the `assets` directory, run:
```console
$ npm install esbuild --save-dev
$ npm install ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view --save
```
or, for Yarn:
```console
$ yarn add --dev esbuild
$ yarn add ../deps/phoenix ../deps/phoenix_html ../deps/phoenix_live_view
```
Next, add a custom JavaScript build script. We'll call the example `assets/build.js`:
```javascript
const esbuild = require("esbuild");
const args = process.argv.slice(2);
const watch = args.includes('--watch');
const deploy = args.includes('--deploy');
const loader = {
// Add loaders for images/fonts/etc, e.g. { '.svg': 'file' }
};
const plugins = [
// Add and configure plugins here
];
// Define esbuild options
let opts = {
entryPoints: ["js/app.js"],
bundle: true,
logLevel: "info",
target: "es2022",
outdir: "../priv/static/assets",
external: ["*.css", "fonts/*", "images/*"],
nodePaths: ["../deps"],
loader: loader,
plugins: plugins,
};
if (deploy) {
opts = {
...opts,
minify: true,
};
}
if (watch) {
opts = {
...opts,
sourcemap: "inline",
};
esbuild
.context(opts)
.then((ctx) => {
ctx.watch();
})
.catch((_error) => {
process.exit(1);
});
} else {
esbuild.build(opts);
}
```
This script covers following use cases:
- `node build.js`: builds for development & testing (useful on CI)
- `node build.js --watch`: like above, but watches for changes continuously
- `node build.js --deploy`: builds minified assets for production
Modify `config/dev.exs` so that the script runs whenever you change files, replacing the existing `:esbuild` configuration under `watchers`:
```elixir
config :hello, HelloWeb.Endpoint,
...
watchers: [
node: ["build.js", "--watch", cd: Path.expand("../assets", __DIR__)]
],
...
```
Modify the `aliases` task in `mix.exs` to install `npm` packages during `mix setup` and use the new `esbuild` on `mix assets.deploy`:
```elixir
defp aliases do
[
setup: ["deps.get", "ecto.setup", "cmd --cd assets npm install"],
...,
"assets.deploy": ["cmd --cd assets node build.js --deploy", "phx.digest"]
]
end
```
Finally, remove the `esbuild` configuration from `config/config.exs` and remove the dependency from the `deps` function in your `mix.exs`, and you are done!
## Alternative JS build tools
If you are writing an API or you want to use another asset build tool, you may want to remove the `esbuild` Hex package (see steps below). Then you must follow the additional steps required by the third-party tool.
### Remove esbuild
1. Remove the `esbuild` configuration in `config/config.exs` and `config/dev.exs`,
2. Remove the `assets.deploy` task defined in `mix.exs`,
3. Remove the `esbuild` dependency from `mix.exs`,
4. Unlock the `esbuild` dependency:
```console
$ mix deps.unlock esbuild
```
## Alternative CSS frameworks
By default, Phoenix generates CSS with the `tailwind` library and its default plugins.
If you want to use external `tailwind` plugins or another CSS framework, you should replace the `tailwind` Hex package (see steps below). Then you can use an `esbuild` plugin (as outlined above) or even bring a separate framework altogether.
### Remove tailwind
1. Remove the `tailwind` configuration in `config/config.exs` and `config/dev.exs`,
2. Remove the `assets.deploy` task defined in `mix.exs`,
3. Remove the `tailwind` dependency from `mix.exs`,
4. Unlock the `tailwind` dependency:
```console
$ mix deps.unlock tailwind
```
You may optionally remove and delete the `heroicons` dependency as well.
## Alternative icon libraries
Phoenix ships with the [Heroicons](https://heroicons.com/) library for icons support.
This is done by embedding icons as CSS classes, which guarantees only the icons actually
used by your application are sent to the client, thanks to Tailwind.
If you prefer to use an alternative icon set, it should be possible to adapt the
code that embeds Heroicons to use another library. Let's see exactly how to do that
using [Remix Icon](https://remixicon.com/) as an example:
First replace the `heroicon` repository in your `mix.exs` by `remixicons`:
```elixir
{:remixicons,
github: "Remix-Design/RemixIcon",
sparse: "icons",
tag: "v4.6.0",
app: false,
compile: false,
depth: 1},
```
Then replace `assets/vendor/heroicons.js`, which traverses the heroicons dependency, by `assets/vendor/remixicons.js`, which traverses remix icons instead:
```js
const plugin = require("tailwindcss/plugin")
const fs = require("fs")
const path = require("path")
module.exports = plugin(function({matchComponents, theme}) {
let baseDir = path.join(__dirname, "../../deps/remixicons/icons");
let values = {};
let icons = fs
.readdirSync(baseDir, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
icons.forEach((dir) => {
fs.readdirSync(path.join(baseDir, dir)).map((file) => {
let name = path.basename(file, ".svg");
values[name] = { name, fullPath: path.join(baseDir, dir, file) };
});
});
matchComponents(
{
ri: ({ name, fullPath }) => {
let content = fs
.readFileSync(fullPath)
.toString()
.replace(/\r?\n|\r/g, "");
return {
[`--ri-${name}`]: `url('data:image/svg+xml;utf8,${content}')`,
"-webkit-mask": `var(--ri-${name})`,
mask: `var(--ri-${name})`,
"background-color": "currentColor",
"vertical-align": "middle",
display: "inline-block",
width: theme("spacing.10"),
height: theme("spacing.10"),
};
},
},
{ values },
);
})
```
And then change `assets/css/app.css` to import your new plugin instead.
Finally, update the `icon` function in `lib/my_app_web/components/core_components.ex`
to match on `ri-` prefixes instead:
```
@doc """
Renders a [Remix Icon](https://remixicon.com).
You can customize the size and colors of the icons by
setting width, height, and background color classes.
## Examples
<.icon name="ri-github-fill" />
<.icon name="ri-github" class="ml-1 w-3 h-3 animate-spin" />
"""
attr :name, :string, required: true
attr :class, :any, default: "size-5"
def icon(%{name: "ri-" <> _} = assigns) do
~H"""
"""
end
```
Now replace the Heroicons in your application by Remix ones and you are good to go!
The approach above may also work with other libraries, it is a matter of adapting
the Tailwind plugin to traverse these libraries and generate the proper classes.
Some iconsets may also be available as regular Hex packages too.
================================================
FILE: guides/authn_authz/api_authentication.md
================================================
# API Authentication
> **Requirement**: This guide expects that you have gone through the [`mix phx.gen.auth`](mix_phx_gen_auth.html) guide.
This guide shows how to add API authentication on top of `mix phx.gen.auth`. Since the authentication generator already includes a token table, we use it to store API tokens too, following the best security practices.
We will break this guide in two parts: augmenting the context and the plug implementation. We will assume that the following `mix phx.gen.auth` command was executed:
```
$ mix phx.gen.auth Accounts User users
```
If you ran something else, it should be trivial to adapt the names.
## Adding API functions to the context
Our authentication system will require two functions. One to create the API token and another to verify it. Open up `lib/my_app/accounts.ex` and add these two new functions:
```elixir
## API
@doc """
Creates a new api token for a user.
The token returned must be saved somewhere safe.
This token cannot be recovered from the database.
"""
def create_user_api_token(user) do
{encoded_token, user_token} = UserToken.build_email_token(user, "api-token")
Repo.insert!(user_token)
encoded_token
end
@doc """
Fetches the user by API token.
"""
def fetch_user_by_api_token(token) do
with {:ok, query} <- UserToken.verify_api_token_query(token),
%User{} = user <- Repo.one(query) do
{:ok, user}
else
_ -> :error
end
end
```
The new functions use the existing `UserToken` functionality to store a new type of token called "api-token". Because this is an email token, if the user changes their email, the tokens will be expired.
Also notice we called the second function `fetch_user_by_api_token`, instead of `get_user_by_api_token`. Because we want to render different status codes in our API, depending if a user was found or not, we return `{:ok, user}` or `:error`. Elixir's convention is to call these functions `fetch_*`, instead of `get_*` which would usually return `nil` instead of tuples.
To make sure our new functions work, let's write tests. Open up `test/my_app/accounts_test.exs` and add this new describe block:
```elixir
describe "create_user_api_token/1 and fetch_user_by_api_token/1" do
test "creates and fetches by token" do
user = user_fixture()
token = Accounts.create_user_api_token(user)
assert Accounts.fetch_user_by_api_token(token) == {:ok, user}
assert Accounts.fetch_user_by_api_token("invalid") == :error
end
end
```
If you run the tests, they will actually fail. Something similar to this:
```console
1) test create_user_api_token/1 and fetch_user_by_api_token/1 creates and fetches by token (Demo.AccountsTest)
test/demo/accounts_test.exs:380
** (UndefinedFunctionError) function Demo.Accounts.UserToken.verify_api_token_query/1 is undefined or private. Did you mean:
* verify_change_email_token_query/2
* verify_magic_link_token_query/1
* verify_session_token_query/1
code: assert Accounts.fetch_user_by_api_token(token) == {:ok, user}
stacktrace:
(demo 0.1.0) Demo.Accounts.UserToken.verify_api_token_query("sTpJg7rt-KQ9gZ7xLMtn2keusGk9N2JpPwkXDx7LmHU")
(demo 0.1.0) lib/demo/accounts.ex:325: Demo.Accounts.fetch_user_by_api_token/1
test/demo/accounts_test.exs:383: (test)
```
If you prefer, try looking at the error and fixing it yourself. The explanation will come next.
The `UserToken` module contains functions for verifying different tokens. Right now, there is no `verify_api_token_query/1`, but we can implement it similar to the existing functions. How long the API token should be valid is going to depend on your application and how sensitive it is in terms of security. For this example, let's say the token is valid for 365 days.
Open up `lib/my_app/accounts/user_token.ex`, and add a new function, like this:
```elixir
@doc """
Checks if the API token is valid and returns its underlying lookup query.
The query returns the user found by the token, if any.
The given token is valid if it matches its hashed counterpart in the
database and the user email has not changed. This function also checks
if the token is being used within 365 days.
"""
def verify_api_token_query(token) do
case Base.url_decode64(token, padding: false) do
{:ok, decoded_token} ->
hashed_token = :crypto.hash(@hash_algorithm, decoded_token)
query =
from token in by_token_and_context_query(hashed_token, "api-token"),
join: user in assoc(token, :user),
where:
token.inserted_at > ago(^@api_token_validity_in_days, "day") and
token.sent_to == user.email,
select: user
{:ok, query}
:error ->
:error
end
end
```
Note that we also added a `@api_token_validity_in_days` module attribute at the top of the file:
```diff
@magic_link_validity_in_minutes 15
@change_email_validity_in_days 7
@session_validity_in_days 60
+ @api_token_validity_in_days 365
```
Now tests should pass and we are ready to move forward!
## API authentication plug
The last part is to add authentication to our API.
When we ran `mix phx.gen.auth`, it generated a `MyAppWeb.UserAuth` module with several plugs, which are small functions that receive the `conn` and customize our request/response life-cycle. Open up `lib/my_app_web/user_auth.ex` and add this new function:
```elixir
def fetch_current_scope_for_api_user(conn, _opts) do
with [<>] <-
get_req_header(conn, "authorization"),
true <- String.downcase(bearer) == "bearer",
{:ok, user} <- Accounts.fetch_user_by_api_token(token) do
assign(conn, :current_scope, Scope.for_user(user))
else
_ ->
conn
|> send_resp(:unauthorized, "No access for you")
|> halt()
end
end
```
Our function receives the connection and checks if the "authorization" header has been set with "Bearer TOKEN", where "TOKEN" is the value returned by `Accounts.create_user_api_token/1`. In case the token is not valid or there is no such user, we abort the request.
Finally, we need to add this `plug` to our pipeline. Open up `lib/my_app_web/router.ex` and you will find a pipeline for API. Let's add our new plug under it, like this:
```elixir
pipeline :api do
plug :accepts, ["json"]
plug :fetch_current_scope_for_api_user
end
```
Now you are ready to receive and validate API requests. Feel free to open up `test/my_app_web/user_auth_test.exs` and write your own test. You can use the tests for other plugs as templates!
## Your turn
The overall API authentication flow will depend on your application.
If you want to use this token in a JavaScript client, you will need to slightly alter the `UserSessionController` to invoke `Accounts.create_user_api_token/1` and return a JSON response including the token.
If you want to provide APIs for 3rd-party users, you will need to allow them to create tokens, and show the result of `Accounts.create_user_api_token/1` to them. They must save these tokens somewhere safe and include them as part of their requests using the "authorization" header.
================================================
FILE: guides/authn_authz/authn_authz.md
================================================
# Introduction to Auth
Authentication (authn) and authorization (authz) are two important concepts in security. Authentication is the process of verifying the identity of a user or system, while authorization is the process of granting or denying access to resources based on the user's identity and permissions.
Phoenix comes with built-in support for both. Generally speaking, developers use the `mix phx.gen.auth` generator to scaffold their authn and authz. Third-party libraries such as [Ueberauth](https://github.com/ueberauth/ueberauth) can be used either as complementary systems or by itself.
Overall we have the following guides:
* [mix phx.gen.auth](mix_phx_gen_auth.md) - An introduction to the `mix phx.gen.auth` generator and its security considerations.
* [Scopes](scopes.md) - Scopes are the mechanism Phoenix v1.8 introduced to manage access to resources based on the user's identity and permissions.
* [API Authentication](api_authentication.md) - An additional guide that shows how to expand `mix phx.gen.auth` code to support token-based API authentication.
================================================
FILE: guides/authn_authz/mix_phx_gen_auth.md
================================================
# mix phx.gen.auth
The `mix phx.gen.auth` command generates a flexible, pre-built authentication system into your Phoenix app. This generator allows you to quickly move past the task of adding authentication to your codebase and stay focused on the real-world problem your application is trying to solve. It supports the following features:
- User registration with account confirmation by email
- Log in with magic links
- Opt-in password authentication
- "Sudo mode", also known as privileged authentication, where the user must confirm their identity before performing sensitive actions
## Getting started
> Before running this command, consider committing your work as it generates multiple files.
Let's start by running the following command from the root of our app:
```console
$ mix phx.gen.auth Accounts User users
An authentication system can be created in two different ways:
- Using Phoenix.LiveView (default)
- Using Phoenix.Controller only
Do you want to create a LiveView based authentication system? [Y/n] Y
```
The first argument is the context module followed by the schema module
and its plural name (used as the schema table name). The example above
will generate an `Accounts` context module with two schemas inside:
`User` and `UserToken`. The context module helps us group all of the
different schemas related to authentication. You may name the context
and schema according to your preferences.
The authentication generators support Phoenix LiveView, for enhanced UX,
so you should answer `Y` here. You may also answer `n` for a controller
based authentication system. Either approach will create the same context
and schemas, using the same table names and route paths.
Since this generator installed additional dependencies in `mix.exs`,
let's fetch those:
```console
$ mix deps.get
```
Now run the pending repository migrations:
```console
$ mix ecto.migrate
```
Let's run the tests to make sure our new authentication system works as expected.
```console
$ mix test
```
And finally, let's start our Phoenix server and try it out (note the new `Register` and `Log in` links at the top right of the default page).
```console
$ mix phx.server
```
## Developer responsibilities
Since Phoenix generates this code into your application instead of building these modules into Phoenix itself, you now have complete freedom to modify the authentication system, so it works best with your use case. The one caveat with using a generated authentication system is it will not be updated after it's been generated. Therefore, as improvements are made to the output of `mix phx.gen.auth`, it becomes your responsibility to determine if these changes need to be ported into your application. Security-related and other important improvements will be explicitly and clearly marked in the `CHANGELOG.md` file and upgrade notes.
## Generated code
The following are notes about the generated authentication system.
### Forbidding access
The generated code ships with an authentication module with a handful of plugs that fetch the current user, require authentication and so on. For instance, in an app named MyApp which had `mix phx.gen.auth Accounts User users` run on it, you will find a module named `MyAppWeb.UserAuth` with plugs such as:
* `fetch_current_scope_for_user` - fetches the current user information if available and stores it as `:current_scope` assign
* `require_authenticated_user` - must be invoked after `fetch_current_scope_for_user` and requires that a current user exists and is authenticated
* `redirect_if_user_is_authenticated` - used for the few pages that must not be available to authenticated users (only generated for controller based authentication)
* `require_sudo_mode` - used for pages that contain sensitive operations and enforces recent authentication
### Scopes
The generated code includes a scope module. For an app named MyApp which had `mix phx.gen.auth Accounts User users` run on it, you will find the following module at `lib/my_app/accounts/scope.ex`:
```elixir
defmodule MyApp.Accounts.Scope do
# ...
alias MyApp.Accounts.User
defstruct user: nil
@doc """
Creates a scope for the given user.
Returns nil if no user is given.
"""
def for_user(%User{} = user) do
%__MODULE__{user: user}
end
def for_user(nil), do: nil
end
```
The scope data structure is stored in the assigns and available to your Controllers and LiveViews. As your application grows in complexity, this data structure can store important metadata such as the teams, companies, or organizations the user belongs to, permissions, telemetry information such as IP address and so forth.
Furthermore, future Phoenix generator invocations will automatically pass this data structure from your Controllers and LiveViews to most of [your context operations](contexts.md), making sure that future data is scoped to the current user/team/company/organization. Scopes are essential to enforce the user can only access data they own. You can learn more about them in the [Scopes](scopes.md) guide.
### Password hashing
The password hashing mechanism defaults to `bcrypt` for Unix systems and `pbkdf2` for Windows systems. Both systems use the [Comeonin interface](https://hexdocs.pm/comeonin/).
The password hashing mechanism can be overridden with the `--hashing-lib` option. The following values are supported:
* `bcrypt` - [bcrypt_elixir](https://hex.pm/packages/bcrypt_elixir)
* `pbkdf2` - [pbkdf2_elixir](https://hex.pm/packages/pbkdf2_elixir)
* `argon2` - [argon2_elixir](https://hex.pm/packages/argon2_elixir)
We recommend developers to consider using `argon2`, which is the most robust of all 3. The downside is that `argon2` is quite CPU and memory intensive, and you will need more powerful instances to run your applications on.
For more information about choosing these libraries, see the [Comeonin project](https://github.com/riverrun/comeonin).
There are similar `:on_mount` hooks for LiveView based authentication.
### Notifiers
The generated code is not integrated with any system to send SMSes or emails for confirming accounts, resetting passwords, etc. Instead, it simply logs a message to the terminal. It is your responsibility to integrate with the proper system after generation.
Note that if you generated your Phoenix project with `mix phx.new`, your project is configured to use [Swoosh](https://hexdocs.pm/swoosh/Swoosh.html) mailer by default. To view notifier emails during development with Swoosh, navigate to `/dev/mailbox`.
### Concurrent tests
The generated tests run concurrently if you are using a database that supports concurrent tests, which is the case of PostgreSQL.
### More about `mix phx.gen.auth`
Check out `mix phx.gen.auth` for more details, such as using a different password hashing library, customizing the web module namespace, generating binary id type, configuring the default options, and using custom table names.
## Security considerations
### Tracking sessions
All sessions and tokens are tracked in a separate table. This allows you to track how many sessions are active for each account. You could even expose this information to users if desired.
Note that whenever the password changes (either via reset password or directly), all tokens are deleted, and the user has to log in again on all devices.
### User Enumeration attacks
A user enumeration attack allows someone to check if an email is registered in the application. The generated authentication code does not attempt to protect from such attacks. For instance, when you register an account, if the email is already registered, the code will notify the user the email is already registered.
If your application is sensitive to enumeration attacks, you need to implement your own workflows, which tends to be very different from most applications, as you need to carefully balance security and user experience.
Furthermore, if you are concerned about enumeration attacks, beware of timing attacks too. For example, registering a new account typically involves additional work (such as writing to the database, sending emails, etc) compared to when an account already exists. Someone could measure the time taken to execute those additional tasks to enumerate emails. This applies to all endpoints (registration, login, etc.) that may send email, in-app notifications, etc.
### Confirmation and credential pre-stuffing attacks
The generated functionality ships with an account confirmation mechanism, where users have to confirm their account, typically by email. Furthermore, to prevent security issues, the generated code does forbid users from using the application if their accounts have not yet been confirmed. If you want to change this behavior, please refer to the ["Mixing magic link and password registration" section](Mix.Tasks.Phx.Gen.Auth.html#module-mixing-magic-link-and-password-registration) of `mix phx.gen.auth`.
### Case sensitiveness
The email lookup is made to be case-insensitive. Case-insensitive lookups are the default in MySQL and MSSQL. In SQLite3 we use [`COLLATE NOCASE`](https://www.sqlite.org/datatype3.html#collating_sequences) in the column definition to support it. In PostgreSQL, we use the [`citext` extension](https://www.postgresql.org/docs/current/citext.html).
Note `citext` is part of PostgreSQL itself and is bundled with it in most operating systems and package managers. `mix phx.gen.auth` takes care of creating the extension and no extra work is necessary in the majority of cases. If by any chance your package manager splits `citext` into a separate package, you will get an error while migrating, and you can most likely solve it by installing the `postgres-contrib` package.
## Additional resources
### Migrating to Phoenix v1.8 magic links and sudo mode
Phoenix v1.8 added new features and simplified the authentication code. Developers are not required to migrate to the new generators, although we recommend setting up your own scope, as defined in the [Scopes](scopes.md) guide.
If you generated your authentication code with `mix phx.gen.auth` in Phoenix v1.7 or earlier and you want to migrate to the new generators, you can use the following pull requests as reference:
* [Pull request for migrating LiveView based Phoenix 1.7 `phx.gen.auth` to magic links](https://github.com/SteffenDE/phoenix_gen_auth_magic_link/pull/1)
* [Pull request for migrating controller based Phoenix 1.7 `phx.gen.auth` to magic links](https://github.com/SteffenDE/phoenix_gen_auth_magic_link/pull/2)
Keep in mind that the new authentication system fully removes registering an account with password, which simplifies both the user experience and the generated code. Therefore, when migrating, you should not change your existing migration files, instead, you must make the `hashed_password` column optional by setting `null: true`. Also, when migrating to the new system and removing features like "Forgot your password?", you must set the `hashed_password` of all accounts that have not been confirmed to `nil`, after making the column nullable, to avoid credential stuffing attacks. For this reason, we recommend deploying the migrated authentication system during low-traffic periods, where ideally no user who has just registered an account would have their password nullified. If those trade-offs are not acceptable, [you can add magic links on top of your existing authentication system without a complete migration, as discussed here](https://github.com/srcrip/phoenix-magic-links).
### Initial implementation
The following links describe the original implementation of the authentication system, the default up to Phoenix v1.7:
* José Valim's blog post - [An upcoming authentication solution for Phoenix](https://dashbit.co/blog/a-new-authentication-solution-for-phoenix)
* Berenice Medel's blog post on generating LiveViews for authentication (rather than conventional Controllers & Views) - [Bringing Phoenix Authentication to Life](https://fly.io/phoenix-files/phx-gen-auth/)
* [Original design spec](https://github.com/dashbitco/mix_phx_gen_auth_demo/blob/auth/README.md)
* [Pull request on bare Phoenix app](https://github.com/dashbitco/mix_phx_gen_auth_demo/pull/1)
================================================
FILE: guides/authn_authz/scopes.md
================================================
# Scopes
A scope is a data structure used to keep information about the current request or session, such as the current user, the user's organization/company, permissions, and so on. Think of a scope as a container with information required by the majority of pages in your application. A scope can also hold request metadata, such as IP addresses.
Scopes play an important role in security. [OWASP](https://owasp.org/) lists "Broken access control" as a [top-10 security risk](https://owasp.org/Top10/). Most application data is private for a user, a team, or an organization. Your database CRUD operations must be properly scoped to the current user/team/organization. Phoenix generators such as `mix phx.gen.html`, `mix phx.gen.json`, and `mix phx.gen.live` automatically use your custom scopes.
Scopes are flexible. You can have more than one scope in your application and choose the specific scope when invoking a generator. When you run `mix phx.gen.auth`, it will automatically generate a scope for you, but you may also add your own.
This guide will:
* Show how `mix phx.gen.auth` generates a scope for you
* Discuss how generators, such as `mix phx.gen.context`, rely on scopes for security
* How to define your own scope from scratch and all valid options
* Augment the built-in scope with additional scopes
## phx.gen.auth
The task `mix phx.gen.auth` will generate a default scope. This scope ties the generated resources to the currently authenticated user. Let's see it in action:
```console
$ mix phx.gen.auth Accounts User users
```
The scope code is the same for the `--live` and `--no-live` variants of the generator.
Looking at the generated scope file `lib/my_app/accounts/scope.ex`, we can see that it defines a struct with a single `user` field, and a function `for_user/1` that, if given a `User` struct, returns a new `%Scope{}` for that user.
```elixir
defmodule MyApp.Accounts.Scope do
alias MyApp.Accounts.User
defstruct user: nil
def for_user(%User{} = user) do
%__MODULE__{user: user}
end
def for_user(nil), do: nil
end
```
The scope is automatically fetched by the `fetch_current_scope_for_user` plug that is injected into the `:browser` pipeline:
```elixir
# router.ex
...
pipeline :browser do
...
plug :fetch_current_scope_for_user
end
```
```elixir
# user_auth.ex
def fetch_current_scope_for_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn)
user = user_token && Accounts.get_user_by_session_token(user_token)
assign(conn, :current_scope, Scope.for_user(user))
end
```
Similarly, for LiveViews, there is a pre-defined `mount_current_scope` hook that ensures
the scope is available:
```elixir
# user_auth.ex
def on_mount(:mount_current_scope, _params, session, socket) do
{:cont, mount_current_scope(socket, session)}
end
defp mount_current_scope(socket, session) do
Phoenix.Component.assign_new(socket, :current_scope, fn ->
user =
if user_token = session["user_token"] do
Accounts.get_user_by_session_token(user_token)
end
Scope.for_user(user)
end)
end
```
## Integration of scopes in the Phoenix generators
If a default scope is defined in your application's config, the generators will build scoped resources by default. The generated LiveViews / Controllers will automatically pass the scope to the context functions. `mix phx.gen.auth` automatically sets its scope as default, if there is not already a default scope defined:
```elixir
# config/config.exs
config :my_app, :scopes,
user: [
default: true,
...
]
```
We will look at the individual options in the next section.
Now let's look at the code generated once a default scope is set. We will use `mix phx.gen.live` as an example, but the ideas and the overall code will be similar to `mix phx.gen.html` and `mix phx.gen.json` too:
```console
$ mix phx.gen.live Blog Post posts title:string body:text
```
This creates a new `Blog` context, with a `Post` resource. To ensure the scope is available, for LiveViews the routes in your `router.ex` must be added to a `live_session` that ensures the user is authenticated. In this case, within the aptly named `required_authenticated_user` section:
```diff
scope "/", MyAppWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :require_authenticated_user,
on_mount: [{MyAppWeb.UserAuth, :ensure_authenticated}] do
live "/users/settings", UserLive.Settings, :edit
live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
+ live "/posts", PostLive.Index, :index
+ live "/posts/new", PostLive.Form, :new
+ live "/posts/:id", PostLive.Show, :show
+ live "/posts/:id/edit", PostLive.Form, :edit
end
post "/users/update-password", UserSessionController, :update_password
end
```
> Although the router has a `scope` macro, the router `scope` and `current_scope` are ultimately distinct features which have similar purposes: to narrow down access to parts of our application, each acting at distinct layers (one at the router, the other at the data layer).
Now, let's look at the generated LiveView (`lib/my_app_web/live/post_live/index.ex`):
```elixir
defmodule MyAppWeb.PostLive.Index do
use MyAppWeb, :live_view
alias MyApp.Blog
...
@impl true
def mount(_params, _session, socket) do
Blog.subscribe_posts(socket.assigns.current_scope)
{:ok,
socket
|> assign(:page_title, "Listing Posts")
|> stream(:posts, Blog.list_posts(socket.assigns.current_scope))}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
post = Blog.get_post!(socket.assigns.current_scope, id)
{:ok, _} = Blog.delete_post(socket.assigns.current_scope, post)
{:noreply, stream_delete(socket, :posts, post)}
end
@impl true
def handle_info({type, %MyApp.Blog.Post{}}, socket)
when type in [:created, :updated, :deleted] do
{:noreply, stream(socket, :posts, Blog.list_posts(socket.assigns.current_scope), reset: true)}
end
end
```
Note that every function from the `Blog` context that we call gets the `current_scope` assign passed in as the first argument. The `list_posts/1` function then uses that information to properly filter posts:
```elixir
# lib/my_app/blog.ex
def list_posts(%Scope{} = scope) do
Repo.all(from post in Post, where: post.user_id == ^scope.user.id)
end
```
The LiveView even subscribes to scoped PubSub messages and automatically updates the rendered list whenever a new post is created or an existing post is updated or deleted, while ensuring that only messages for the current scope are processed.
## Defining scopes
The Phoenix generators use your application's config to discover the available scopes. A scope is defined by the following options:
```elixir
config :my_app, :scopes,
user: [
default: true,
module: MyApp.Accounts.Scope,
assign_key: :current_scope,
access_path: [:user, :id],
schema_key: :user_id,
schema_type: :id,
schema_table: :users,
test_data_fixture: MyApp.AccountsFixtures,
test_setup_helper: :register_and_log_in_user
]
```
In this example, the scope is called `user` and it is the `default` scope that is automatically used when running `mix phx.gen.schema`, `mix phx.gen.context`, `mix phx.gen.live`, `mix phx.gen.html` and `mix phx.gen.json`. A scope needs a module that defines a struct, in this case `MyApp.Accounts.Scope`. Those structs are used as first argument to the generated context functions, like `list_posts/1`.
* `default` - a boolean that indicates if this scope is the default scope. There can only be one default scope defined.
* `module` - the module that defines the struct for this scope.
* `assign_key` - the key where the scope struct is assigned to the [socket](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.Socket.html#t:t/0) or [conn](https://hexdocs.pm/plug/Plug.Conn.html).
* `access_path` - a list of keys that define the path to the identifying field in the scope struct. The generators generate code like `where: schema_key == ^scope.user.id`.
* `route_prefix` - (optional) a path template string for how resources should be nested. For example, `/organizations/:org` would generate routes like `/organizations/:org/posts`. The parameter segment (`:org`) will be replaced with the appropriate scope access value in templates and LiveViews.
* `route_access_path` - (optional) list of keys that define the path to the field used in route generation (if `route_prefix` is set). This is particularly useful for user-friendly URLs where you might want to use a slug instead of an ID. If not specified, it defaults to `Enum.drop(scope.access_path, -1)` or `access_path` if the former is empty. For example, if the `access_path` is `[:organization, :id]`, it defaults to `[:organization]`, assuming that the value at `scope.organization` implements the `Phoenix.Param` protocol.
* `schema_key` - the foreign key that ties the resource to the scope. New scoped schemas are created with a foreign key field named `schema_key` of type `schema_type` to the `schema_table` table.
* `schema_type` - the type of the foreign key field in the schema. Typically `:id` or `:binary_id`.
* `schema_migration_type` (optional) - the type of the foreign key column in the database. Used for the generated migration. It defaults to the default migration foreign keytype.
* `schema_table` - the name of the table where the foreign key points to.
* `test_data_fixture` - a module that is automatically imported into the context test file. It must have a `NAME_scope_fixture/0` function that returns a unique scope struct for context tests, in this case `user_scope_fixture/0`.
* `test_setup_helper` - the name of a function that is registered as [`setup` callback](https://hexdocs.pm/ex_unit/ExUnit.Callbacks.html#setup/1) in LiveView / Controller tests. The function is expected to be imported in the test file. Usually, this is ensured by putting it into the `MyAppWeb.ConnCase` module.
While the `mix phx.gen.auth` automatically generates a scope, scopes can also be defined manually. This can be useful, for example, to retrofit an existing application with scopes or to define scopes that are not tied to a user.
For this example, we will implement a custom scope that gives each session its own scope. While this might not be useful in most real-world applications as created resources would be inaccessible as soon as the session ends, it is a good example to understand how scopes work. See the following section for an example on how to augment an existing scope with organizations (teams, companies, or similar).
First, let's define our scope module `lib/my_app/scope.ex`:
```elixir
defmodule MyApp.Scope do
defstruct id: nil
def for_id(id) do
%MyApp.Scope{id: id}
end
end
```
Next, we define a plug in our router that assigns a scope to each request:
```diff
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
+ plug :assign_scope
end
+
+ defp assign_scope(conn, _opts) do
+ if id = get_session(conn, :scope_id) do
+ assign(conn, :current_scope, MyApp.Scope.for_id(id))
+ else
+ id = System.unique_integer()
+
+ conn
+ |> put_session(:scope_id, id)
+ |> assign(:current_scope, MyApp.Scope.for_id(id))
+ end
+ end
```
For tests, we'll also define a fixture module `test/support/fixtures/scope_fixtures.ex`:
```elixir
defmodule MyApp.ScopeFixtures do
alias MyApp.Scope
def session_scope_fixture(id \\ System.unique_integer()) do
%Scope{id: id}
end
end
```
And then add a `setup` helper to our `test/support/conn_case.ex`:
```elixir
defmodule MyAppWeb.ConnCase do
...
def put_scope_in_session(%{conn: conn}) do
id = System.unique_integer()
scope = MyApp.ScopeFixtures.session_scope_fixture(id)
conn =
conn
|> Phoenix.ConnTest.init_test_session(%{})
|> Plug.Conn.put_session(:scope_id, id)
%{conn: conn, scope: scope}
end
end
```
Finally, we configure the scope in our application's `config/config.exs`:
```elixir
config :my_app, :scopes,
session: [
default: true,
module: MyApp.Scope,
assign_key: :current_scope,
access_path: [:id],
schema_key: :session_id,
schema_type: :id,
schema_migration_type: :bigint,
schema_table: nil,
test_data_fixture: MyApp.ScopeFixtures,
test_setup_helper: :put_scope_in_session
]
```
Setting `schema_table` to `nil` means that the generated resources don't have a foreign key to the scope, but instead a normal `bigint` column that directly stores the scope's id.
We can now generate a new resource, for example with `phx.gen.html`:
```console
$ mix phx.gen.html Post posts title:string
```
When you now visit [http://localhost:4000/posts](http://localhost:4000/posts), and create a new post, you will see that it is only visible to the current session. If you open a private browser window and visit the same URL, the previously created post is not visible. Similarly, if you create a new post in the private window, it is not visible in the other window. If you try to copy the URL of a post created in one session and access it in another, you will get an `Ecto.NoResultsError` error, which is automatically converted to 404 when the `debug_errors` setting is disabled.
## Augmenting scopes
Let's assume that you used `mix phx.gen.auth` to generate a scope tied to users. But now you also create a new `organization` entity, where users can be members of:
```elixir
defmodule MyApp.Accounts.Organization do
use Ecto.Schema
import Ecto.Changeset
@derive {Phoenix.Param, key: :slug}
schema "organizations" do
field :name, :string
field :slug, :string
...
many_to_many :users, MyApp.Accounts.User, join_through: "organizations_users"
timestamps(type: :utc_datetime)
end
end
```
First, we'd adjust our scope struct to also include the organization:
```diff
defmodule MyApp.Accounts.Scope do
alias MyApp.Accounts.User
alias MyApp.Accounts.Organization
- defstruct user: nil
+ defstruct user: nil, organization: nil
def for_user(%User{} = user) do
%__MODULE__{user: user}
end
def for_user(nil), do: nil
+
+ def put_organization(%__MODULE__{} = scope, %Organization{} = organization) do
+ %{scope | organization: organization}
+ end
end
```
Let's also assume that the current organization is part of the URL path, like `http://localhost:4000/organizations/foo/posts`. Then, we'd adjust our router to fetch the organization from the path and assign it to the scope:
```diff
# router.ex
pipeline :browser do
...
plug :fetch_current_scope_for_user
+ plug :assign_org_to_scope
end
```
```elixir
# user_auth.ex
def assign_org_to_scope(conn, _opts) do
current_scope = conn.assigns.current_scope
if slug = conn.params["org"] do
org = MyApp.Accounts.get_organization_by_slug!(current_scope, slug)
assign(conn, :current_scope, MyApp.Accounts.Scope.put_organization(current_scope, org))
else
conn
end
end
```
For LiveViews, we'll also need to add a new `:on_mount` hook and add it to `live_session`'s `on_mount` option in the router:
```diff
# router.ex
scope "/", MyAppWeb do
pipe_through [:browser]
live_session :current_user,
on_mount: [
{MyAppWeb.UserAuth, :mount_current_scope},
+ {MyAppWeb.UserAuth, :assign_org_to_scope}
] do
...
end
end
```
```elixir
# user_auth.ex
def on_mount(:assign_org_to_scope, %{"org" => slug}, _session, socket) do
socket =
case socket.assigns.current_scope do
%{organization: nil} = scope ->
org = MyApp.Accounts.get_organization_by_slug!(socket.assigns.current_scope, slug)
Phoenix.Component.assign(socket, :current_scope, Scope.put_organization(scope, org))
_ ->
socket
end
{:cont, socket}
end
def on_mount(:assign_org_to_scope, _params, _session, socket), do: {:cont, socket}
```
This way, if a route is defined like `live /organizations/:org/posts`, the `assign_org_to_scope` plug would fetch the organization from the path and assign it to the scope. This code assumes that `get_organization_by_slug!/2` raises an `Ecto.NoResultsError` which would be automatically converted to `404`, but you could also handle the error explicitly and, for example, set an error flash and redirect to another page, like a dashboard. The `get_organization_by_slug!/2` function should also rely on the current scope to filter the organizations to those the user has access to.
Then, we are ready to define a new scope in our application's `config/config.exs` to generate resources scoped to the organization:
```elixir
config :my_app, :scopes,
user: [
...
],
organization: [
module: MyApp.Accounts.Scope,
assign_key: :current_scope,
access_path: [:organization, :id],
route_prefix: "/organizations/:org",
route_access_path: [:organization, :slug],
schema_key: :org_id,
schema_type: :id,
schema_table: :organizations,
test_data_fixture: MyApp.AccountsFixtures,
test_setup_helper: :register_and_log_in_user_with_org
]
```
For the generated tests, we'll also need to define a fixture in `test/support/fixtures/accounts_fixtures.ex` and extend our `test/support/conn_case.ex`:
```elixir
defmodule MyApp.AccountsFixtures do
...
def organization_scope_fixture(scope \\ user_scope_fixture()) do
org = organization_fixture(scope)
Scope.put_organization(scope, org)
end
end
```
```elixir
defmodule MyAppWeb.ConnCase do
...
def register_and_log_in_user_with_org(context) do
%{conn: conn, user: _user, scope: scope} = register_and_log_in_user(context)
%{conn: conn, scope: MyApp.AccountsFixtures.organization_scope_fixture(scope)}
end
end
```
Now that our scope configuration includes the `route_prefix`, we can generate resources scoped to the organization, and all paths will be automatically generated with the correct organization slug:
```console
$ mix phx.gen.live Blog Post posts title:string body:text --scope organization
```
This shows that scopes are quite flexible, allowing you to keep a well-defined data structure, even when your application grows.
Most of the time, your application will have a single scope module, like in this example. But sometimes, you might want to create a new scope module, for example to completely separate a user-facing scope from an admin scope, where also the context functions are supposed to only be called by one of the two.
## Scope helpers
When working with more complex scopes, it is often useful to create some helper functions, which can conveniently be added to the scope module:
```elixir
defmodule MyApp.Accounts.Scope do
alias MyApp.Accounts
alias MyApp.Accounts.{User, Organization}
defstruct user: nil, organization: nil
def for_user(%User{} = user) do
%__MODULE__{user: user}
end
def for_user(nil), do: nil
def put_organization(%__MODULE__{} = scope, %Organization{} = organization) do
%{scope | organization: organization}
end
def for(opts) when is_list(opts) do
cond do
opts[:user] && opts[:org] ->
user = user(opts[:user])
org = org(opts[:org])
user
|> for_user()
|> put_organization(org)
opts[:user] ->
user = user(opts[:user])
for_user(user)
opts[:org] ->
%__MODULE__{organization: org(opts[:org])}
end
end
defp user(id) when is_integer(id) do
Accounts.get_user!(id)
end
defp user(email) when is_binary(email) do
Accounts.get_user_by_email(email)
end
defp org(id) when is_integer(id) do
Accounts.get_organization!(id)
end
defp org(slug) when is_binary(slug) do
Accounts.get_organization_by_slug!(slug)
end
end
```
Then, you can alias the Scope module in your project's `.iex.exs`:
```elixir
alias MyApp.Accounts.Scope
```
And when working with scoped context functions, you can just do:
```elixir
iex> MyApp.Blog.list_posts(Scope.for(user: 1, org: "foo"))
...
iex> MyApp.Accounts.list_api_tokens(Scope.for(user: "john@doe.com"))
...
```
================================================
FILE: guides/cheatsheets/router.cheatmd
================================================
# Routing cheatsheet
> Those need to be declared in the correct router module and scope.
A quick reference to the common routing features' syntax. For an exhaustive overview, refer to the [routing guides](routing.md).
## Routing declaration
{: .col-2}
### Single route
```elixir
get "/users", UserController, :index
patch "/users/:id", UserController, :update
```
```elixir
# Verified Routes
~p"/users"
~p"/users/#{@user}"
```
Also accepts `put`, `patch`, `options`, `delete` and `head`.
### Resources
#### Simple
```elixir
resources "/users", UserController
```
Generates `:index`, `:edit`, `:new`, `:show`, `:create`, `:update` and `:delete`.
#### Options
```elixir
resources "/users", UserController, only: [:show]
resources "/users", UserController, except: [:create, :delete]
```
#### Nested
```elixir
resources "/users", UserController do
resources "/posts", PostController
end
```
```elixir
# Verified Routes
~p"/users"
~p"/users/new"
~p"/users/#{@user}"
~p"/users/#{@user}/edit"
~p"/users/#{@user}/posts"
~p"/users/#{@user}/posts/new"
~p"/users/#{@user}/posts/#{@post}"
~p"/users/#{@user}/posts/#{@post}/edit"
```
For more info check the [resources docs.](routing.html#resources)
### Scopes
#### Simple
```elixir
scope "/admin", HelloWeb.Admin do
pipe_through :browser
resources "/users", UserController
end
```
```elixir
# Verified Routes
~p"/admin/users"
~p"/admin/users/new"
~p"/admin/users/#{@user}"
~p"/admin/users/#{@user}/edit"
```
#### Nested
```elixir
scope "/api", HelloWeb.Api do
pipe_through :api
scope "/v1", V1 do
resources "/users", UserController
end
end
```
```elixir
# Verified Routes
~p"/api/v1/users"
~p"/api/v1/users/new"
~p"/api/v1/users/#{@user}"
~p"/api/v1/users/#{@user}/edit"
```
For more info check the [scoped routes](routing.md#scoped-routes) docs.
================================================
FILE: guides/components.md
================================================
# Components and HEEx
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
> **Requirement**: This guide expects that you have gone through the [request life-cycle guide](request_lifecycle.html).
The Phoenix endpoint pipeline takes a request, routes it to a controller, and calls a view module to render a template. The view interface from the controller is simple – the controller calls a view function with the connection's assigns, and the function's job is to return a HEEx template. We call any function that accepts an `assigns` parameter and returns a HEEx template a *function component*.
> The Phoenix framework is designed for HTML applications, JSON APIs, GraphQL endpoints, etc. For this reason, all of the functionality related to HTML rendering comes as part of two separate packages:
>
> * [`phoenix_html`](https://hexdocs.pm/phoenix_html) - defines the building blocks for writing HTML safely. In your project, you'll interact primarily with the `Phoenix.HTML` module, which is imported by default in all templates
>
> * [`phoenix_live_view`](https://hexdocs.pm/phoenix_live_view) - a library for rich, real-time user experiences with server-rendered HTML. While LiveView provides several abstraction for building collaborative and dynamic applications, it also defines the `HEEx` template language, function components, and JS commands, which brings powerful abstractions for all kinds of server-rendered HTML applications
In this chapter, we will recap how components are used and dig deeper to discover new use cases.
## Function components
Function components are the essential building block for any kind of markup-based template rendering you'll perform in Phoenix. They serve as a shared abstraction for the standard MVC controller-based applications, LiveView applications, layouts, and smaller UI definitions you'll use throughout other templates. Their documentation is available in [the `Phoenix.Component` module](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html).
At the end of the Request life-cycle chapter, we created a template at `lib/hello_web/controllers/hello_html/show.html.heex`, let's open it up:
```heex
Hello World, from {@messenger}!
```
`` is a function component defined inside `lib/hello_web/components/layouts.ex`. If you open the file up, you will find:
```elixir
def app(assigns) do
~H"""
...
```
So far, all of the function components we have defined also worked as templates. Let's learn more about them by defining our own component with the intent of encapsulating some HTML markup.
Imagine we want to refactor our `show.html.heex` to move the rendering of `
Hello World, from {@messenger}!
` to its own function. Remember that `show.html.heex` is embedded within the `HelloHTML` module. Let's open it up:
```elixir
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
embed_templates "hello_html/*"
end
```
That's simple enough. There's only two lines, `use HelloWeb, :html`. This line calls the `html/0` function defined in `HelloWeb` which sets up the basic imports and configuration for our function components and templates. All of the imports and aliases in our module will also be available in our templates. Similarly, if we want to write a function component to be invoked from `show.html.heex`, we can simply add it to `HelloHTML`. Let's do so:
```elixir
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
embed_templates "hello_html/*"
attr :messenger, :string, required: true
def greet(assigns) do
~H"""
Hello World, from {@messenger}!
"""
end
end
```
We declared the attributes we accept via the [`Phoenix.Component.attr/3`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#attr/3) macro, then we defined our `greet/1` function which returns the HEEx template.
Next we need to update `show.html.heex`:
```heex
<.greet messenger={@messenger} />
```
When we reload `http://localhost:4000/hello/Frank`, we should see the same content as before! The `show.html.heex` is now invoking two different function components:
* ``. If the component was defined elsewhere, we would need to give its full name, such as: ``.
By declaring attributes as required, Phoenix will warn if we call the `<.greet />` component without passing attributes. If an attribute is optional, you can specify the `:default` option with a value:
```
attr :messenger, :string, default: nil
```
Overall, function components are the essential building block of Phoenix rendering stack. Next, let's fully understand the expressive power behind the HEEx template language.
## HEEx
Function components and templates files are powered by [the HEEx template language](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2), which stands for "HTML + Embedded Elixir". We can write Elixir code inside `{...}` for HTML-aware interpolation inside tag attributes and the body, as done above. For example, we use `@name` to access the key `name` defined inside `assigns`.
We can also interpolate arbitrary HEEx blocks using `<%= ... %>`. This is often used for block constructs. For example, in order to have conditionals:
```heex
<%= if some_condition? do %>
Some condition is true for user: {@messenger}
<% else %>
Some condition is false for user: {@messenger}
<% end %>
```
or even loops:
```heex
Number
Power
<%= for number <- 1..10 do %>
{number}
{number * number}
<% end %>
```
HEEx also comes with handy HTML extensions we will learn next.
### HTML extensions
Besides allowing interpolation of Elixir expressions, `.heex` templates come with HTML-aware extensions. For example, let's see what happens if you try to interpolate a value with "<" or ">" in it, which would lead to HTML injection:
```heex
{"Bold?"}
```
Once you render the template, you will see the literal `` on the page. This means users cannot inject HTML content on the page. If you want to allow them to do so, you can call `raw`, but do so with extreme care:
```heex
{raw("Bold?")}
```
Another super power of HEEx templates is validation of HTML and interpolation syntax of attributes. You can write:
```heex
Hello {@username}
```
Notice how you could simply use `key={value}`. HEEx will automatically handle special values such as `false` to remove the attribute or a list of classes.
To interpolate a dynamic number of attributes in a keyword list or map, do:
```heex
Hello {@username}
```
Also, try removing the closing `` or renaming it to ``. HEEx templates will let you know about your error.
HEEx also supports shorthand syntax for `if` and `for` expressions via the special `:if` and `:for` attributes. For example, rather than this:
```heex
<%= if @some_condition do %>
...
<% end %>
```
You can write:
```heex
...
```
Likewise, for comprehensions may be written as:
```heex
{item.name}
```
## CoreComponents
In a new Phoenix application, you will also find a `core_components.ex` module inside the `components` folder. This module is a great example of defining function components to be reused throughout our application. This guarantees that, as our application evolves, our components will look consistent.
If you look inside `def html` in `HelloWeb` placed at `lib/hello_web.ex`, you will see that `CoreComponents` are automatically imported into all HTML views via `use HelloWeb, :html`. This is also the reason why `CoreComponents` itself performs `use Phoenix.Component` instead of `use HelloWeb, :html` at the top: doing the latter would cause a deadlock as we would try to import `CoreComponents` into itself.
CoreComponents also play an important role in Phoenix code generators, as the code generators assume those components are available in order to quickly scaffold your application. In case you want to learn more about all of these pieces, you may:
* Explore the generated `CoreComponents` module to learn more from practical examples
* Read the official documentation for [`Phoenix.Component`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html)
* Read the official documentation for [HEEx and the ~H sigils](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2)
* If you are looking for higher level components beyond the minimal ones included by Phoenix, [the LiveView project keeps a list of component systems](https://github.com/phoenixframework/phoenix_live_view#component-systems)
## Layouts
When talking about components and rendering in Phoenix, it is important to understand the concept of layouts.
All Phoenix applications have one component called the "root layout". This page is where you will find the `` and `` tags of your HTML page. The root layout is configured in your `lib/hello_web/router.ex` file:
```elixir
plug :put_root_layout, html: {HelloWeb.Layouts, :root}
```
In a newly generated app, the template itself can be found at `lib/hello_web/components/layouts/root.html.heex`. Open it up and, just about at the end of the ``, you will see this:
```heex
{@inner_content}
```
That's where our templates are injected once they are rendered. The root layout is reused by controllers and live views alike.
Any dynamic functionality of your application is then implemented as function components. For example, your application menu and sidebar is typically part of the `app` component in `lib/hello_web/components/layouts.ex`, which is invoked in every template:
```heex
...
```
This mechanism is also very flexible. For example, if you want to create an admin layout, you can simply add a new function in the `Layouts` module, and then invoke `Layouts.admin` instead of `Layouts.app`:
```heex
...
```
> Previous Phoenix versions used a nested layout mechanism, by passing the `:layouts` to `Phoenix.Controller` and `:layout` to `Phoenix.LiveView`, but this mechanism is discouraged in new Phoenix applications.
================================================
FILE: guides/controllers.md
================================================
# Controllers
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
> **Requirement**: This guide expects that you have gone through the [request life-cycle guide](request_lifecycle.html).
Phoenix controllers act as intermediary modules. Their functions — called actions — are invoked from the router in response to HTTP requests. The actions, in turn, gather all the necessary data and perform all the necessary steps before invoking the view layer to render a template or returning a JSON response.
Phoenix controllers also build on the Plug package, and are themselves plugs. Controllers provide the functions to do almost anything we need to in an action. If we do find ourselves looking for something that Phoenix controllers don't provide, we might find what we're looking for in Plug itself. Please see the [Plug guide](plug.html) or the [Plug documentation](`Plug`) for more information.
A newly generated Phoenix app will have a single controller named `PageController`, which can be found at `lib/hello_web/controllers/page_controller.ex` which looks like this:
```elixir
defmodule HelloWeb.PageController do
use HelloWeb, :controller
def home(conn, _params) do
render(conn, :home)
end
end
```
The first line below the module definition invokes the `__using__/1` macro of the `HelloWeb` module, which imports some useful modules.
`PageController` gives us the `home` action to display the Phoenix [welcome page] associated with the default route Phoenix defines in the router.
## Actions
Controller actions are just functions. We can name them anything we like as long as they follow Elixir's naming rules. The only requirement we must fulfill is that the action name matches a route defined in the router.
For example, in `lib/hello_web/router.ex` we could change the action name in the default route that Phoenix gives us in a new app from `home`:
```elixir
get "/", PageController, :home
```
to `index`:
```elixir
get "/", PageController, :index
```
as long as we change the action name in `PageController` to `index` as well, the [welcome page] will load as before.
```elixir
defmodule HelloWeb.PageController do
...
def index(conn, _params) do
render(conn, :home)
end
end
```
While we can name our actions whatever we like, there are conventions for action names which we should follow whenever possible. We went over these in the [routing guide](routing.html), but we'll take another quick look here.
- index - renders a list of all items of the given resource type
- show - renders an individual item by ID
- new - renders a form for creating a new item
- create - receives parameters for one new item and saves it in a data store
- edit - retrieves an individual item by ID and displays it in a form for editing
- update - receives parameters for one edited item and saves the item to a data store
- delete - receives an ID for an item to be deleted and deletes it from a data store
Each of these actions takes two parameters, which will be provided by Phoenix behind the scenes.
The first parameter is always `conn`, a struct which holds information about the request such as the host, path elements, port, query string, and much more. `conn` comes to Phoenix via Elixir's Plug middleware framework. More detailed information about `conn` can be found in the [Plug.Conn documentation](`Plug.Conn`).
The second parameter is `params`. Not surprisingly, this is a map which holds any parameters passed along in the HTTP request. It is a good practice to pattern match against parameters in the function signature to provide data in a simple package we can pass on to rendering. We saw this in the [request life-cycle guide](request_lifecycle.html) when we added a messenger parameter to our `show` route in `lib/hello_web/controllers/hello_controller.ex`.
```elixir
defmodule HelloWeb.HelloController do
...
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
end
```
In some cases — often in `index` actions, for instance — we don't care about parameters because our behavior doesn't depend on them. In those cases, we don't use the incoming parameters, and simply prefix the variable name with an underscore, calling it `_params`. This will keep the compiler from complaining about the unused variable while still keeping the correct arity.
## Rendering
Controllers can render content in several ways. The simplest is to render some plain text using the [`text/2`] function which Phoenix provides.
For example, let's rewrite the `show` action from `HelloController` to return text instead. For that, we could do the following.
```elixir
def show(conn, %{"messenger" => messenger}) do
text(conn, "From messenger #{messenger}")
end
```
Now [`/hello/Frank`] in your browser should display `From messenger Frank` as plain text without any HTML.
A step beyond this is rendering pure JSON with the [`json/2`] function. We need to pass it something that the [Jason library](`Jason`) can decode into JSON, such as a map. (Jason is one of Phoenix's dependencies.)
```elixir
def show(conn, %{"messenger" => messenger}) do
json(conn, %{id: messenger})
end
```
If we again visit [`/hello/Frank`] in the browser, we should see a block of JSON with the key `id` mapped to the string `"Frank"`.
```json
{"id": "Frank"}
```
The [`json/2`] function is useful for writing APIs and there is also the [`html/2`] function for rendering HTML, but most of the times we use Phoenix views to build our responses. For this, Phoenix includes the [`render/3`] function. It is specially important for HTML responses, as Phoenix Views provide performance and security benefits.
Let's rollback our `show` action to what we originally wrote in the [request life-cycle guide](request_lifecycle.html):
```elixir
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
end
```
In order for the [`render/3`] function to work correctly, the controller and view must share the same root name (in this case `Hello`), and the `HelloHTML` module must include an `embed_templates` definition specifying where its templates live. By default the controller, view module, and templates are collocated together in the same controller directory. In other words, `HelloController` requires `HelloHTML`, and `HelloHTML` requires the existence of the `lib/hello_web/controllers/hello_html/` directory, which must contain the `show.html.heex` template.
[`render/3`] will also pass the value which the `show` action received for `messenger` from the parameters as an assign.
If we need to pass values into the template when using `render`, that's easy. We can pass a keyword like we've seen with `messenger: messenger`, or we can use `Plug.Conn.assign/3`, which conveniently returns `conn`.
```elixir
def show(conn, %{"messenger" => messenger}) do
conn
|> Plug.Conn.assign(:messenger, messenger)
|> render(:show)
end
```
Note: Using `Phoenix.Controller` imports `Plug.Conn`, so shortening the call to [`assign/3`] works just fine.
Passing more than one value to our template is as simple as connecting [`assign/3`] functions together:
```elixir
def show(conn, %{"messenger" => messenger}) do
conn
|> assign(:messenger, messenger)
|> assign(:receiver, "Dweezil")
|> render(:show)
end
```
Or you can pass the assigns directly to `render` instead:
```elixir
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger, receiver: "Dweezil")
end
```
Generally speaking, once all assigns are configured, we invoke the view layer. The view layer (`HelloWeb.HelloHTML`) then renders `show.html` alongside the layout and a response is sent back to the browser.
## New rendering formats
Rendering HTML through a template is fine, but what if we need to change the rendering format on the fly? Let's say that sometimes we need HTML, sometimes we need plain text, and sometimes we need JSON. Then what?
The view's job is not only to render HTML templates. Views are about data presentation. Given a bag of data, the view's purpose is to present that in a meaningful way given some format, be it HTML, JSON, CSV, or others. Many web apps today return JSON to remote clients, and Phoenix views are *great* for JSON rendering.
As an example, let's take `PageController`'s `home` action from a newly generated app. Out of the box, this has the right view `PageHTML`, the embedded templates from (`lib/hello_web/controllers/page_html`), and the right template for rendering HTML (`home.html.heex`.)
```elixir
def home(conn, _params) do
render(conn, :home)
end
```
What it doesn't have is a view for rendering JSON. Phoenix Controller hands off to a view module to render templates, and it does so per format. We already have a view for the HTML format, but we need to instruct Phoenix how to render the JSON format as well. By default, you can see which formats your controllers support in `lib/hello_web.ex`:
```elixir
def controller do
quote do
use Phoenix.Controller,
formats: [:html, :json]
...
end
end
```
So out of the box Phoenix will look for a `HTML` and `JSON` view modules based on the request format and the controller name. We can also explicitly tell Phoenix in our controller which view(s) to use for each format. For example, what Phoenix does by default can be explicitly set with the following in your controller:
```elixir
plug :put_view, html: HelloWeb.PageHTML, json: HelloWeb.PageJSON
```
Let's add a `PageJSON` view module at `lib/hello_web/controllers/page_json.ex`:
```elixir
defmodule HelloWeb.PageJSON do
def home(_assigns) do
%{message: "this is some JSON"}
end
end
```
Since the Phoenix View layer is simply a function that the controller renders, passing connection assigns, we can define a regular `home/1` function and return a map to be serialized as JSON.
There are just a few more things we need to do to make this work. Because we want to render both HTML and JSON from the same controller, we need to tell our router that it should accept the `json` format. We do that by adding `json` to the list of accepted formats in the `:browser` pipeline. Let's open up `lib/hello_web/router.ex` and change `plug :accepts` to include `json` as well as `html` like this.
```elixir
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html", "json"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
...
```
Phoenix allows us to change formats on the fly with the `_format` query string parameter. If we go to [`http://localhost:4000/?_format=json`](http://localhost:4000/?_format=json), we will see `%{"message": "this is some JSON"}`.
In practice, however, applications that need to render both formats typically use two distinct pipelines for each, such as the `pipeline :api` already defined in your router file. To learn more, see [our JSON and APIs guide](json_and_apis.md).
### Sending responses directly
If none of the rendering options above quite fits our needs, we can compose our own using some of the functions that `Plug` gives us. Let's say we want to send a response with a status of "201" and no body whatsoever. We can do that with the `Plug.Conn.send_resp/3` function.
Edit the `home` action of `PageController` in `lib/hello_web/controllers/page_controller.ex` to look like this:
```elixir
def home(conn, _params) do
send_resp(conn, 201, "")
end
```
Reloading [http://localhost:4000](http://localhost:4000) should show us a completely blank page. The network tab of our browser's developer tools should show a response status of "201" (Created). Some browsers (Safari) will download the response, as the content type is not set.
To be specific about the content type, we can use [`put_resp_content_type/2`] in conjunction with [`send_resp/3`].
```elixir
def home(conn, _params) do
conn
|> put_resp_content_type("text/plain")
|> send_resp(201, "")
end
```
Using `Plug` functions in this way, we can craft just the response we need.
### Setting the content type
Analogous to the `_format` query string param, we can render any sort of format we want by modifying the HTTP Content-Type Header and providing the appropriate template.
If we wanted to render an XML version of our `home` action, we might implement the action like this in `lib/hello_web/controllers/page_controller.ex`.
```elixir
def home(conn, _params) do
conn
|> put_resp_content_type("text/xml")
|> put_format(:xml)
|> render(:home, content: some_xml_content)
end
```
Then we would need to provide a `home.xml.eex` template that creates XML and a `PageXML` view that embeds the template, and that would be it.
Note: The `home.xml.eex` template uses the `.eex` extension. `.eex` templates are rendered by [`EEx`](https://hexdocs.pm/eex/main/EEx.html).
For a list of valid content mime-types, please see the `MIME` library.
### Setting the HTTP Status
We can also set the HTTP status code of a response similarly to the way we set the content type. The `Plug.Conn` module, imported into all controllers, has a `put_status/2` function to do this.
`Plug.Conn.put_status/2` takes `conn` as the first parameter and as the second parameter either an integer or a "friendly name" used as an atom for the status code we want to set. The list of status code atom representations can be found in `Plug.Conn.Status.code/1` documentation.
Let's change the status in our `PageController` `home` action.
```elixir
def home(conn, _params) do
conn
|> put_status(202)
|> render(:home)
end
```
The status code we provide must be a valid number.
## Redirection
Often, we need to redirect to a new URL in the middle of a request. A successful `create` action, for instance, will usually redirect to the `show` action for the resource we just created. Alternately, it could redirect to the `index` action to show all the things of that same type. There are plenty of other cases where redirection is useful as well.
Whatever the circumstance, Phoenix controllers provide the handy [`redirect/2`] function to make redirection easy. Phoenix differentiates between redirecting to a path within the application and redirecting to a URL — either within our application or external to it.
In order to try out [`redirect/2`], let's create a new route in `lib/hello_web/router.ex`.
```elixir
defmodule HelloWeb.Router do
...
scope "/", HelloWeb do
...
get "/", PageController, :home
get "/redirect_test", PageController, :redirect_test
...
end
end
```
Then we'll change `PageController`'s `home` action of our controller to do nothing but to redirect to our new route.
```elixir
defmodule HelloWeb.PageController do
use HelloWeb, :controller
def home(conn, _params) do
redirect(conn, to: ~p"/redirect_test")
end
end
```
We made use of `Phoenix.VerifiedRoutes.sigil_p/2` to build our redirect path, which is the preferred approach to reference any path within our application. We learned about verified routes in the [routing guide](routing.html).
Finally, let's define in the same file the action we redirect to, which simply renders the home, but now under a new address:
```elixir
def redirect_test(conn, _params) do
render(conn, :home)
end
```
When we reload our [welcome page], we see that we've been redirected to `/redirect_test` which shows the original welcome page. It works!
If we care to, we can open up our developer tools, click on the network tab, and visit our root route again. We see two main requests for this page - a get to `/` with a status of `302`, and a get to `/redirect_test` with a status of `200`.
Notice that the redirect function takes `conn` as well as a string representing a relative path within our application. For security reasons, the `:to` option can only redirect to paths within your application. If you want to redirect to a fully-qualified path or an external URL, you should use `:external` instead:
```elixir
def home(conn, _params) do
redirect(conn, external: "https://elixir-lang.org/")
end
```
## Flash messages
Sometimes we need to communicate with users during the course of an action. Maybe there was an error updating a schema, or maybe we just want to welcome them back to the application. For this, we have flash messages.
The `Phoenix.Controller` module provides the [`put_flash/3`] to set flash messages as a key-value pair and placing them into a `@flash` assign in the connection. Let's set two flash messages in our `HelloWeb.PageController` to try this out.
To do this we modify the `home` action as follows:
```elixir
defmodule HelloWeb.PageController do
...
def home(conn, _params) do
conn
|> put_flash(:error, "Let's pretend we have an error.")
|> render(:home)
end
end
```
In order to see our flash messages, we need to be able to retrieve them and display them in a template layout. We can do that using [`Phoenix.Flash.get/2`] which takes the flash data and the key we care about. It then returns the value for that key.
For our convenience, a `flash_group` component is already available and added to the beginning of our [welcome page]
```heex
<.flash_group flash={@flash} />
```
When we reload the [welcome page], our message should appear in the top right corner of the page.
The flash functionality is handy when mixed with redirects. Perhaps you want to redirect to a page with some extra information. If we reuse the redirect action from the previous section, we can do:
```elixir
def home(conn, _params) do
conn
|> put_flash(:error, "Let's pretend we have an error.")
|> redirect(to: ~p"/redirect_test")
end
```
Now if you reload the [welcome page], you will be redirected and the flash message will be shown once more.
Besides [`put_flash/3`], the `Phoenix.Controller` module has another useful function worth knowing about. [`clear_flash/1`] takes only `conn` and removes any flash messages which might be stored in the session.
Phoenix does not enforce which keys are stored in the flash. As long as we are internally consistent, all will be well. `:info` and `:error`, however, are common and are handled by default in our templates.
## Error pages
Phoenix has two views called `ErrorHTML` and `ErrorJSON` which live in `lib/hello_web/controllers/`. The purpose of these views is to handle errors in a general way for incoming HTML or JSON requests. Similar to the views we built in this guide, error views can return both HTML and JSON responses. See the [Custom Error Pages How-To](custom_error_pages.html) for more information.
[`render/4`]: `Phoenix.Template.render/4`
[`/hello/Frank`]: http://localhost:4000/hello/Frank
[`assign/3`]: `Plug.Conn.assign/3`
[`clear_flash/1`]: `Phoenix.Controller.clear_flash/1`
[`Phoenix.Flash.get/2`]: `Phoenix.Flash.get/2`
[`html/2`]: `Phoenix.Controller.html/2`
[`json/2`]: `Phoenix.Controller.json/2`
[`put_flash/3`]: `Phoenix.Controller.put_flash/3`
[`put_resp_content_type/2`]: `Plug.Conn.put_resp_content_type/2`
[`put_root_layout/2`]: `Phoenix.Controller.put_root_layout/2`
[`redirect/2`]: `Phoenix.Controller.redirect/2`
[`render/3`]: `Phoenix.Controller.render/3`
[`send_resp/3`]: `Plug.Conn.send_resp/3`
[`text/2`]: `Phoenix.Controller.text/2`
[welcome page]: http://localhost:4000/
================================================
FILE: guides/data_modelling/contexts.md
================================================
# 1. Intro to Contexts
Phoenix guides are broken into several major sections. The main building blocks are outlined under the "Core Concepts" section, where we explored [the request life-cycle](request_lifecycle.html), wired up controller actions through our routers, and learned how Ecto allows data to be validated and persisted. Now it's time to tie it all together by writing web-facing features that interact with our greater Elixir application.
When building a Phoenix project, we are first and foremost building an Elixir application. Phoenix's job is to provide a web interface into our Elixir application. Naturally, we compose our applications with modules and functions, but we often assign specific responsibilities to certain modules and give them names: such as controllers, routers, and live views.
However, the most important part of your web application is often where we encapsulate data access and data validation. We call these modules **contexts**. They often talk to a database, using `Ecto`, or APIs, using an HTTP client such as `Req`. By giving these modules a name, we help developers identify these patterns and talk about them. At the end of the day, contexts are just modules, as are your controllers, views, etc.
If you have used `mix phx.gen.html`, `mix phx.gen.json`, or `mix phx.gen.live`, you have already used contexts. For example, run the following generator in a Phoenix application:
```console
$ mix phx.gen.live Post posts title body:text
```
The command above will output a few files, among them, a `MyApp.Posts.Post` schema in `lib/my_app/posts/post.ex`, which outlines how the resource is represented in the database, and a **context** module named `MyApp.Posts` that encapsulates all the database access to said schema. The `MyApp.Posts` module centralizes all functionality related to posts, instead of scattering logic around controllers, LiveViews, etc.
Contexts are also useful to nest resources. For example, if you are adding comments to your posts, you can colocate their schemas, since comments belong to posts, like this:
```console
$ mix phx.gen.live Posts Comment comments post_id:references:posts body:text
```
The first argument to the generator above is the context module, instructing Phoenix to colocate the comments functionality with posts. There is also a `post_id` attribute which specifies a foreign key reference to the posts table. As your application grows, contexts help you group related schemas, instead of having several dozens of schemas with no insights on how they relate to each other.
Developers may also use contexts to intentionally name parts of their application. For example, `mix phx.gen.auth` requires a context name to be explicitly given. It is often invoked as:
```console
$ mix phx.gen.auth Accounts User users
```
or, using whatever name you prefer, such as:
```console
$ mix phx.gen.auth Identity Client clients
```
The generated `Accounts` (or `Identity`) context will encapsulate all functionality for managing users (or clients) and their tokens. You could, if you wanted, name the context `Users` too, but given account/identity management has well defined name and boundary in most applications, giving it an explicit name makes its purposes clear. And, at the end of the day, they are just plain modules.
In this guide, we will use these ideas to build out our web application. Our goal is to build an ecommerce system where we can showcase products, allow users to add products to their cart, and complete their orders. We will do so by intentionally designing and naming our contexts. Opposite to other Phoenix guides, **these guides are meant to be read in order**.
## Our ecommerce application
Let's start an application from scratch to build our ecommerce, using Phoenix Express. We will call the application `hello`.
For macOS/Ubuntu:
```bash
$ curl https://new.phoenixframework.org/hello | sh
```
For Windows PowerShell:
```bash
curl.exe -fsSO https://new.phoenixframework.org/hello.bat; .\hello.bat
```
If those commands do not work, see the [Installation Guide](installation.html) and then run `mix phx.new`:
```console
$ mix phx.new hello
```
Follow any of the steps printed on the screen and open up the generated `hello` project in your editor.
We are ready to move to the next chapter.
================================================
FILE: guides/data_modelling/cross_context_boundaries.md
================================================
# 4. Cross-context Boundaries
Now that we have the beginnings of our product catalog features, let's begin to work on the other main features of our application – carting products from the catalog. In order to properly track products that have been added to a user's cart, we'll need a new place to persist this information, along with point-in-time product information like the price at time of carting. This is necessary so we can detect product price changes in the future. We know what we need to build, but now we need to decide where the cart functionality lives in our application.
If we take a step back and think about the isolation of our application, the exhibition of products in our catalog distinctly differs from the responsibilities of managing a user's cart. A product catalog shouldn't care about the rules of our shopping cart system, and vice-versa. There's a clear need here for a separate context to handle the new cart responsibilities. Let's call it `ShoppingCart`.
Let's create a `ShoppingCart` context to handle basic cart duties. Before we write code, let's imagine we have the following feature requirements:
1. Add products to a user's cart from the product show page
2. Store point-in-time product price information at time of carting
3. Store and update quantities in cart
4. Calculate and display sum of cart prices
From the description, it's clear we need a `Cart` resource for storing the user's cart, along with a `CartItem` to track products in the cart. With our plan set, let's get to work.
## Adding authentication
Most of the cart functionality is tied to a specific user. Therefore, in order to allow each user to manage their own cart (and only their own carts), we must be able to authenticate users. To do so, we will use Phoenix's built-in `mix phx.gen.auth` generator to scaffold a solution for us:
```console
mix phx.gen.auth Accounts User users
```
You will see output similar to the following:
```console
An authentication system can be created in two different ways:
- Using Phoenix.LiveView (default)
- Using Phoenix.Controller only
Do you want to create a LiveView based authentication system? [Yn]
```
Type `n` followed by `Return` key,
you will see output similar to:
```console
...
* creating lib/hello/accounts/scope.ex
...
* injecting config/config.exs
...
Please re-fetch your dependencies with the following command:
$ mix deps.get
Remember to update your repository by running migrations:
$ mix ecto.migrate
Once you are ready, visit "/users/register"
to create your account and then access "/dev/mailbox" to
see the account confirmation email.
```
After following the instructions to re-fetch dependencies and migrating the database, we can start the server with `mix phx.server` and re-visit the home page [`http://localhost:4000/`](http://localhost:4000/). There, we should see new registration and login links at the top of the page. On the registration page, create a new user. In development, a confirmation email is sent to the dev mailbox, which is accessible at [`http://localhost:4000/dev/mailbox`](http://localhost:4000/dev/mailbox). After clicking the confirmation link, you should be successfully logged in.
One of the benefits of `mix phx.gen.auth` is that it also generates a scope file at `lib/hello/accounts/scope.ex`. In a nutshell, authentication tells us who a user is based on their email address, but it doesn't tell us the resources the user owns or has access to. In order to do so, we need authorization. Scopes help us tie generated resources, such as the Cart we will create, to users. Let's open up the file:
```elixir
defmodule Hello.Accounts.Scope do
...
alias Hello.Accounts.User
defstruct user: nil
@doc """
Creates a scope for the given user.
Returns nil if no user is given.
"""
def for_user(%User{} = user) do
%__MODULE__{user: user}
end
def for_user(nil), do: nil
end
```
We can see that it is simply a struct with a `user` field. The authentication system ensures that the `current_scope` assign is accordingly set with the current user. Let's see it in practice.
## Generating scoped resources
Let's generate our new context:
```console
$ mix phx.gen.context ShoppingCart Cart carts
* creating lib/hello/shopping_cart/cart.ex
* creating priv/repo/migrations/20250205203128_create_carts.exs
* creating lib/hello/shopping_cart.ex
* injecting lib/hello/shopping_cart.ex
* creating test/hello/shopping_cart_test.exs
* injecting test/hello/shopping_cart_test.exs
* creating test/support/fixtures/shopping_cart_fixtures.ex
* injecting test/support/fixtures/shopping_cart_fixtures.ex
Remember to update your repository by running migrations:
$ mix ecto.migrate
```
We generated our new context `ShoppingCart`, with a new `ShoppingCart.Cart` schema. Open up the generated schema and migration files and you will see it has automatically included a `user_id` field, thanks to the scope. Furthermore, when we explore the code later on, we will learn all queries to the carts table have been properly scoped.
With our cart in place, let's generate our cart items. This time we will pass the `--no-scope` flag, because we will associate `cart_items` to `carts` and the `carts` are already scoped to the user:
```console
$ mix phx.gen.context ShoppingCart CartItem cart_items \
cart_id:references:carts product_id:references:products \
price_when_carted:decimal quantity:integer --no-scope
You are generating into an existing context.
...
Would you like to proceed? [Yn] y
* creating lib/hello/shopping_cart/cart_item.ex
* creating priv/repo/migrations/20250205213410_create_cart_items.exs
* injecting lib/hello/shopping_cart.ex
* injecting test/hello/shopping_cart_test.exs
* injecting test/support/fixtures/shopping_cart_fixtures.ex
Remember to update your repository by running migrations:
$ mix ecto.migrate
```
We generated a new resource inside our `ShoppingCart` named `CartItem`. This schema and table will hold references to a cart and product, along with the price at the time we added the item to our cart, and the quantity the user wishes to purchase. Let's touch up the generated migration file in `priv/repo/migrations/*_create_cart_items.ex`:
```diff
create table(:cart_items) do
- add :price_when_carted, :decimal
+ add :price_when_carted, :decimal, precision: 15, scale: 6, null: false
add :quantity, :integer
- add :cart_id, references(:carts, on_delete: :nothing)
+ add :cart_id, references(:carts, on_delete: :delete_all)
- add :product_id, references(:products, on_delete: :nothing)
+ add :product_id, references(:products, on_delete: :delete_all)
timestamps(type: :utc_datetime)
end
- create index(:cart_items, [:cart_id])
create index(:cart_items, [:product_id])
+ create unique_index(:cart_items, [:cart_id, :product_id])
```
We used the `:delete_all` strategy again to enforce data integrity. This way, when a cart or product is deleted from the application, we don't have to rely on application code in our `ShoppingCart` or `Catalog` contexts to worry about cleaning up the records. This keeps our application code decoupled and the data integrity enforcement where it belongs – in the database. We also added a unique constraint to ensure a duplicate product is not allowed to be added to a cart. As with the `product_categories` table, using a multi-column index lets us remove the separate index for the leftmost field (`cart_id`). With our database tables in place, we can now migrate up:
```console
$ mix ecto.migrate
16:59:51.941 [info] == Running 20250205203342 Hello.Repo.Migrations.CreateCarts.change/0 forward
16:59:51.945 [info] create table carts
16:59:51.952 [info] == Migrated 20250205203342 in 0.0s
16:59:51.988 [info] == Running 20250205213410 Hello.Repo.Migrations.CreateCartItems.change/0 forward
16:59:51.988 [info] create table cart_items
16:59:52.000 [info] create index cart_items_product_id_index
16:59:52.001 [info] create index cart_items_cart_id_product_id_index
16:59:52.002 [info] == Migrated 20250205213410 in 0.0s
```
Our database is ready to go with new `carts` and `cart_items` tables, but now we need to map that back into application code. You may be wondering how we can mix database foreign keys across different tables and how that relates to the context pattern of isolated, grouped functionality. Let's jump in and discuss the approaches and their tradeoffs.
## Cross-context data
So far, we've done a great job isolating the two main contexts of our application from each other, but now we have a necessary dependency to handle.
Our `Catalog.Product` resource serves to keep the responsibilities of representing a product inside the catalog, but ultimately for an item to exist in the cart, a product from the catalog must be present. Given this, our `ShoppingCart` context will have a data dependency on the `Catalog` context. With that in mind, we have two options. One is to expose APIs on the `Catalog` context that allows us to efficiently fetch product data for use in the `ShoppingCart` system, which we would manually stitch together. Or we can use database joins to fetch the dependent data. Both are valid options given your tradeoffs and application size, but joining data from the database when you have a hard data dependency is just fine for a large class of applications and is the approach we will take here.
Now that we know where our data dependencies exist, let's add our schema associations so we can tie shopping cart items to products. First, let's make a quick change to our cart schema in `lib/hello/shopping_cart/cart.ex` to associate a cart to its items:
```diff
schema "carts" do
- field :user_id, :id
+ belongs_to :user, Hello.Accounts.User
+ has_many :items, Hello.ShoppingCart.CartItem
timestamps(type: :utc_datetime)
end
```
Now that our cart is associated to the items we place in it, let's set up the cart item associations inside `lib/hello/shopping_cart/cart_item.ex`:
```diff
schema "cart_items" do
field :price_when_carted, :decimal
field :quantity, :integer
- field :cart_id, :id
- field :product_id, :id
+ belongs_to :cart, Hello.ShoppingCart.Cart
+ belongs_to :product, Hello.Catalog.Product
timestamps(type: :utc_datetime)
end
@doc false
def changeset(cart_item, attrs) do
cart_item
- |> cast(attrs, [:price_when_carted, :quantity])
+ |> cast(attrs, [:quantity])
- |> validate_required([:price_when_carted, :quantity])
+ |> validate_required([:quantity])
+ |> validate_number(:quantity, greater_than_or_equal_to: 0, less_than: 100)
end
```
First, we replaced the `cart_id` field with a standard `belongs_to` pointing at our `ShoppingCart.Cart` schema. Next, we replaced our `product_id` field by adding our first cross-context data dependency with a `belongs_to` for the `Catalog.Product` schema. Here, we intentionally coupled the data boundaries because it provides exactly what we need: an isolated context API with the bare minimum knowledge necessary to reference a product in our system. Second, we remove `price_when_carted` from the list of permitted attributes to ensure any price provided by user input is discarded. Finally, we added a new validation to our changeset. With `validate_number/3`, we ensure any quantity provided by user input is between 0 and 100.
With our schemas in place, we can start integrating the new data structures and `ShoppingCart` context APIs into our web-facing features.
## Adding Cart functions
As we mentioned before, the context generators are only a starting point for our application. We can and should write well-named, purpose built functions to accomplish the goals of our context. We have a few new features to implement. First, we need to ensure every user of our application is granted a cart if one does not yet exist. From there, we can then allow users to add items to their cart, update item quantities, and calculate cart totals. Let's get started!
Because we used `mix phx.gen.auth`, we already have a real authentication system in place. We can use the `current_scope` assign to access the currently authenticated user. Let's add a new plug that assigns a cart if there is an authenticated user:
```diff
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_scope_for_user
+ plug :fetch_current_cart
end
+ alias Hello.ShoppingCart
+
+ defp fetch_current_cart(%{assigns: %{current_scope: scope}} = conn, _opts) when not is_nil(scope) do
+ if cart = ShoppingCart.get_cart(scope) do
+ assign(conn, :cart, cart)
+ else
+ {:ok, new_cart} = ShoppingCart.create_cart(scope, %{})
+ assign(conn, :cart, new_cart)
+ end
+ end
+
+ defp fetch_current_cart(conn, _opts), do: conn
```
We added a new `:fetch_current_cart` plug which either finds a cart for the user UUID or creates a cart for the current user and assigns the result in the connection assigns. We'll need to implement our `ShoppingCart.get_cart/1`, but let's add our routes first.
We'll need to implement a cart controller for handling cart operations like viewing a cart, updating quantities, and initiating the checkout process, as well as a cart items controller for adding and removing individual items to and from the cart. The authentication system already generated different router scopes that have different authentication requirements:
```elixir
...
## Authentication routes
scope "/", HelloWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/user/register", UserRegistrationController, :new
post "/user/register", UserRegistrationController, :create
end
scope "/", HelloWeb do
pipe_through [:browser, :require_authenticated_user]
get "/user/settings", UserSettingsController, :edit
put "/user/settings", UserSettingsController, :update
get "/user/settings/confirm-email/:token", UserSettingsController, :confirm_email
end
...
```
As you can see, the registration route has a `:redirect_if_user_is_authenticated` plug, which means it will redirect to the home page if the user is already authenticated. The user settings routes use a `:require_authenticated_user` plug, which means they will redirect to the log in page if the user is not authenticated. These plugs are defined in the `lib/hello_web/user_auth.ex` module.
For our cart routes, we want to only allow access to authenticated users. Add the following routes to your router in `lib/hello_web/router.ex`:
```diff
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :index
resources "/products", ProductController
end
+ scope "/", HelloWeb do
+ pipe_through [:browser, :require_authenticated_user]
+
+ resources "/cart_items", CartItemController, only: [:create, :delete]
+
+ get "/cart", CartController, :show
+ put "/cart", CartController, :update
+ end
```
We added a `resources` declaration for a `CartItemController`, which will wire up the routes for a create and delete action for adding and removing individual cart items. Next, we added two new routes pointing at a `CartController`. The first route, a GET request, will map to our show action, to show the cart contents. The second route, a PUT request, will handle the submission of a form for updating our cart quantities.
With our routes in place, let's add the ability to add an item to our cart from the product show page. Create a new file at `lib/hello_web/controllers/cart_item_controller.ex` and key this in:
```elixir
defmodule HelloWeb.CartItemController do
use HelloWeb, :controller
alias Hello.ShoppingCart
def create(conn, %{"product_id" => product_id}) do
case ShoppingCart.add_item_to_cart(conn.assigns.current_scope, conn.assigns.cart, product_id) do
{:ok, _item} ->
conn
|> put_flash(:info, "Item added to your cart")
|> redirect(to: ~p"/cart")
{:error, _changeset} ->
conn
|> put_flash(:error, "There was an error adding the item to your cart")
|> redirect(to: ~p"/cart")
end
end
def delete(conn, %{"id" => product_id}) do
{:ok, _cart} = ShoppingCart.remove_item_from_cart(conn.assigns.current_scope, conn.assigns.cart, product_id)
redirect(conn, to: ~p"/cart")
end
end
```
We defined a new `CartItemController` with the create and delete actions that we declared in our router. For `create`, we call a `ShoppingCart.add_item_to_cart/3` function which we'll implement in a moment. If successful, we show a flash successful message and redirect to the cart show page; else, we show a flash error message and redirect to the cart show page. For `delete`, we'll call a `remove_item_from_cart` function which we'll implement on our `ShoppingCart` context and then redirect back to the cart show page. We haven't implemented these two shopping cart functions yet, but notice how their names scream their intent: `add_item_to_cart` and `remove_item_from_cart` make it obvious what we are accomplishing here. It also allows us to spec out our web layer and context APIs without thinking about all the implementation details at once.
Let's implement the new interface for the `ShoppingCart` context API in `lib/hello/shopping_cart.ex`:
```diff
+ alias Hello.Catalog
- alias Hello.ShoppingCart.Cart
+ alias Hello.ShoppingCart.{Cart, CartItem}
alias Hello.Accounts.Scope
+ def get_cart(%Scope{} = scope) do
+ Repo.one(
+ from(c in Cart,
+ where: c.user_id == ^scope.user.id,
+ left_join: i in assoc(c, :items),
+ left_join: p in assoc(i, :product),
+ order_by: [asc: i.inserted_at],
+ preload: [items: {i, product: p}]
+ )
+ )
+ end
def create_cart(%Scope{} = scope, attrs) do
with {:ok, cart = %Cart{}} <-
%Cart{}
|> Cart.changeset(attrs, scope)
|> Repo.insert() do
broadcast(scope, {:created, cart})
- {:ok, cart}
+ {:ok, get_cart(scope)}
end
end
+
+ def add_item_to_cart(%Scope{} = scope, %Cart{} = cart, product_id) do
+ true = cart.user_id == scope.user.id
+ product = Catalog.get_product!(product_id)
+
+ %CartItem{quantity: 1, price_when_carted: product.price}
+ |> CartItem.changeset(%{})
+ |> Ecto.Changeset.put_assoc(:cart, cart)
+ |> Ecto.Changeset.put_assoc(:product, product)
+ |> Repo.insert(
+ on_conflict: [inc: [quantity: 1]],
+ conflict_target: [:cart_id, :product_id]
+ )
+ end
+
+ def remove_item_from_cart(%Scope{} = scope, %Cart{} = cart, product_id) do
+ true = cart.user_id == scope.user.id
+
+ {1, _} =
+ Repo.delete_all(
+ from(i in CartItem,
+ where: i.cart_id == ^cart.id,
+ where: i.product_id == ^product_id
+ )
+ )
+
+ {:ok, get_cart(scope)}
+ end
```
We started by implementing `get_cart/1` which fetches our cart and joins the cart items, and their products so that we have the full cart populated with all preloaded data. Next, we modified our `create_cart` function to use `get_cart` to reload the cart contents.
Next, we wrote our new `add_item_to_cart/3` function which accepts a scope, a cart struct and a product id. We proceed to fetch the product with `Catalog.get_product!/1`, showing how contexts can naturally invoke other contexts if required. You could also have chosen to receive the product as argument and you would achieve similar results. Then we used an upsert operation against our repo to either insert a new cart item into the database, or increase the quantity by one if it already exists in the cart. This is accomplished via the `on_conflict` and `conflict_target` options, which tells our repo how to handle an insert conflict.
Finally, we implemented `remove_item_from_cart/3` where we simply issue a `Repo.delete_all` call with a query to delete the cart item in our cart that matches the product ID. Finally, we reload the cart contents by calling `get_cart/1`.
With our new cart functions in place, we can now expose the "Add to cart" button on the product catalog show page. Open up your template in `lib/hello_web/controllers/product_html/show.html.heex` and make the following changes:
```diff
...
<.button variant="primary" navigate={~p"/products/#{@product}/edit?return_to=show"}>
<.icon name="hero-pencil-square" /> Edit product
+ <.button href={~p"/cart_items?product_id=#{@product.id}"} method="post">
+ Add to cart
+
...
```
The `link` function component from `Phoenix.Component` accepts a `:method` attribute to issue an HTTP verb when clicked, instead of the default GET request. With this link in place, the "Add to cart" link will issue a POST request, which will be matched by the route we defined in router which dispatches to the `CartItemController.create/2` function.
Let's try it out. Start your server with `mix phx.server` and visit a product page. If we try clicking the add to cart link, we'll be greeted by an error page. If you are authenticated the following logs should be visible in the console:
```text
[info] POST /cart_items
[debug] Processing with HelloWeb.CartItemController.create/2
Parameters: %{"_method" => "post", "product_id" => "1", ...}
Pipelines: [:browser, :require_authenticated_user]
[debug] QUERY OK source="user_tokens" db=2.4ms idle=1340.8ms
...
[debug] QUERY OK source="cart_items" db=2.5ms
INSERT INTO "cart_items" ...
[info] Sent 302 in 24ms
[info] GET /cart
[debug] Processing with HelloWeb.CartController.show/2
Parameters: %{}
Pipelines: [:browser, :require_authenticated_user]
[debug] QUERY OK source="user_tokens" db=1.6ms idle=430.2ms
...
[debug] QUERY OK source="carts" db=1.9ms idle=1798.5ms
...
[info] Sent 500 in 18ms
[error] ** (UndefinedFunctionError) function HelloWeb.CartController.init/1 is undefined (module HelloWeb.CartController is not available)
...
```
It's working! Kind of. If we follow the logs, we see our POST to the `/cart_items` path. Next, we can see our `ShoppingCart.add_item_to_cart` function successfully inserted a row into the `cart_items` table, and then we issued a redirect to `/cart`. Before our error, we also see a query to the `carts` table, which means we're fetching the current user's cart. So far so good. We know our `CartItem` controller and new `ShoppingCart` context functions are doing their jobs, but we've hit our next unimplemented feature when the router attempts to dispatch to a nonexistent cart controller. Let's create the cart controller, view, and template to display and manage user carts.
Create a new file at `lib/hello_web/controllers/cart_controller.ex` and key this in:
```elixir
defmodule HelloWeb.CartController do
use HelloWeb, :controller
alias Hello.ShoppingCart
def show(conn, _params) do
render(conn, :show, changeset: ShoppingCart.change_cart(conn.assigns.current_scope, conn.assigns.cart))
end
end
```
We defined a new cart controller to handle the `get "/cart"` route. For showing a cart, we render a `"show.html"` template which we'll create in a moment. We know we need to allow the cart items to be changed by quantity updates, so right away we know we'll need a cart changeset. Fortunately, the context generator included a `ShoppingCart.change_cart/1` function, which we'll use. We pass it our cart struct which is already in the connection assigns thanks to the `fetch_current_cart` plug we defined in the router.
Next, we can implement the view and template. Create a new view file at `lib/hello_web/controllers/cart_html.ex` with the following content:
```elixir
defmodule HelloWeb.CartHTML do
use HelloWeb, :html
alias Hello.ShoppingCart
embed_templates "cart_html/*"
def currency_to_str(%Decimal{} = val), do: "$#{Decimal.round(val, 2)}"
end
```
We created a view to render our `show.html` template and aliased our `ShoppingCart` context so it will be in scope for our template. We'll need to display the cart prices like product item price, cart total, etc, so we defined a `currency_to_str/1` which takes our decimal struct, rounds it properly for display, and prepends a USD dollar sign.
Next we can create the template at `lib/hello_web/controllers/cart_html/show.html.heex`:
```heex
<.header>
My Cart
<:subtitle :if={@cart.items == []}>Your cart is empty
<.button navigate={~p"/products"}>Back to products
```
We started by showing the empty cart message if our preloaded `cart.items` is empty. If we have items, we use the `form` component provided by our `HelloWeb.CoreComponents` to take our cart changeset that we assigned in the `CartController.show/2` action and create a form which maps to our cart controller `update/2` action. Within the form, we use the [`inputs_for`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#inputs_for/1) component to render inputs for the nested cart items. This will allow us to map item inputs back together when the form is submitted. Next, we display a number input for the item quantity and label it with the product title. We finish the item form by converting the item price to string. We haven't written the `ShoppingCart.total_item_price/1` function yet, but again we employed the idea of clear, descriptive public interfaces for our contexts. After rendering inputs for all the cart items, we show an "update cart" submit button, along with the total price of the entire cart. This is accomplished with another new `ShoppingCart.total_cart_price/1` function which we'll implement in a moment. Finally, we added a `back` component to go back to our products page.
We're almost ready to try out our cart page, but first we need to implement our new currency calculation functions. Open up your shopping cart context at `lib/hello/shopping_cart.ex` and add these new functions:
```elixir
def total_item_price(%CartItem{} = item) do
Decimal.mult(item.product.price, item.quantity)
end
def total_cart_price(%Cart{} = cart) do
Enum.reduce(cart.items, 0, fn item, acc ->
item
|> total_item_price()
|> Decimal.add(acc)
end)
end
```
We implemented `total_item_price/1` which accepts a `%CartItem{}` struct. To calculate the total price, we simply take the preloaded product's price and multiply it by the item's quantity. We used `Decimal.mult/2` to take our decimal currency struct and multiply it with proper precision. Similarly for calculating the total cart price, we implemented a `total_cart_price/1` function which accepts the cart and sums the preloaded product prices for items in the cart. We again make use of the `Decimal` functions to add our decimal structs together.
Now that we can calculate price totals, let's try it out! Visit [`http://localhost:4000/cart`](http://localhost:4000/cart) and you should already see your first item in the cart. Going back to the same product and clicking "add to cart" will show our upsert in action. Your quantity should now be two. Nice work!
Our cart page is almost complete, but submitting the form will yield yet another error.
```text
[info] POST /cart
...
[error] ** (UndefinedFunctionError) function HelloWeb.CartController.update/2 is undefined or private
```
Let's head back to our `CartController` at `lib/hello_web/controllers/cart_controller.ex` and implement the update action:
```elixir
def update(conn, %{"cart" => cart_params}) do
case ShoppingCart.update_cart(conn.assigns.current_scope, conn.assigns.cart, cart_params) do
{:ok, _cart} ->
redirect(conn, to: ~p"/cart")
{:error, _changeset} ->
conn
|> put_flash(:error, "There was an error updating your cart")
|> redirect(to: ~p"/cart")
end
end
```
We started by plucking out the cart params from the form submit. Next, we call our existing `ShoppingCart.update_cart/2` function which was added by the context generator. We'll need to make some changes to this function, but the interface is good as is. If the update is successful, we redirect back to the cart page, otherwise we show a flash error message and send the user back to the cart page to fix any mistakes. Out-of-the-box, our `ShoppingCart.update_cart/2` function only concerned itself with casting the cart params into a changeset and updates it against our repo. For our purposes, we now need it to handle nested cart item associations, and most importantly, business logic for how to handle quantity updates like zero-quantity items being removed from the cart.
Head back over to your shopping cart context in `lib/hello/shopping_cart.ex` and replace your `update_cart/2` function with the following implementation:
```elixir
def update_cart(%Scope{} = scope, %Cart{} = cart, attrs) do
true = cart.user_id == scope.user.id
changeset =
cart
|> Cart.changeset(attrs, scope)
|> Ecto.Changeset.cast_assoc(:items, with: &CartItem.changeset/2)
Repo.transact(fn ->
with {:ok, cart} <- Repo.update(changeset),
{_count, _cart_items} = Repo.delete_all(from(i in CartItem, where: i.cart_id == ^cart.id and i.quantity == 0)) do
{:ok, cart}
end
end)
|> case do
{:ok, cart} ->
broadcast_cart(scope, {:updated, cart})
{:ok, cart}
{:error, reason} ->
{:error, reason}
end
end
```
We started much like how our out-of-the-box code started – we take the cart struct and cast the user input to a cart changeset, except this time we use `Ecto.Changeset.cast_assoc/3` to cast the nested item data into `CartItem` changesets. Remember the [`<.inputs_for />`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#inputs_for/1) call in our cart form template? That hidden ID data is what allows Ecto's `cast_assoc` to map item data back to existing item associations in the cart.
Once the `changeset` is ready, we wrap everything in `Repo.transact/2` so the operations run safely as one. Inside the transaction, we update the cart using `Repo.update/1`. If the update succeeds, we follow up with a cleanup step using `Repo.delete_all/2` to remove any cart items with zero quantity. Running both steps in the same transaction prevents partial updates and keeps the cart data accurate. Finally, we broadcast the updated cart so that any connected LiveViews can instantly show the changes.
Let's head back to the browser and try it out. Add a few products to your cart, update the quantities, and watch the values changes along with the price calculations. Setting any quantity to 0 will also remove the item. You can also try logging out and registering a new user to see how the carts are scoped to the current user. Pretty neat!
================================================
FILE: guides/data_modelling/faq.md
================================================
# 6. FAQ
Here we list frequently asked questions about contexts.
## When to use code generators?
In this guide, we have used code generators for schemas, contexts, controllers, and more. If you are happy to move forward with Phoenix defaults, feel free to rely on generators to scaffold large parts of your application. When using Phoenix generators, the main question you need to answer is: does this new functionality (with its schema, table, and fields) belong to one of the existing contexts or a new one?
This way, Phoenix generators guide you to use contexts to group related functionality, instead of having several dozens of schemas laying around without any structure. And remember: if you're stuck when trying to come up with a context name, you can simply use the plural form of the resource you're creating.
## How do I structure code inside contexts?
You may wonder how to organize the code inside contexts. For example, should you define a module for changesets (such as ProductChangesets) and another module for queries (such as ProductQueries)?
One important benefit of contexts is that this decision does not matter much. The context is your public API, the other modules are private. Contexts isolate these modules into small groups so the surface area of your application is the context and not _all of your code_.
So while you and your team could establish patterns for organizing these private modules, it is also our opinion that it is completely fine for them to be different. The major focus should be on how the contexts are defined and how they interact with each other (and with your web application).
Think about it as a well-kept neighbourhood. Your contexts are houses, you want to keep them well-preserved, well-connected, etc. Inside the houses, they may all be a little bit different, and that's fine.
## Returning Ecto structures from context APIs
As we explored the context API, you might have wondered:
> If one of the goals of our context is to encapsulate Ecto Repo access, why does `create_user/1` return an `Ecto.Changeset` struct when we fail to create a user?
Although Changesets are part of Ecto, they are not tied to the database, and they can be used to map data from and to any source, which makes it a general and useful data structure for tracking field changes, perform validations, and generate error messages.
For those reasons, `%Ecto.Changeset{}` is a good choice to model the data changes between your contexts and your web layer - regardless if you are talking to an API or the database.
Finally, note that your controllers and views are not hardcoded to work exclusively with Ecto either. Instead, Phoenix defines protocols such as `Phoenix.Param` and `Phoenix.HTML.FormData`, which allow any library to extend how Phoenix generates URL parameters or renders forms. Conveniently for us, the `phoenix_ecto` project implements those protocols, but you could as well bring your own data structures and implement them yourself.
================================================
FILE: guides/data_modelling/in_context_relationships.md
================================================
# 3. In-context Relationships
Our basic catalog features are nice, but let's take it up a notch by categorizing products. Many ecommerce solutions allow products to be categorized in different ways, such as a product being marked for fashion, power tools, and so on. Starting with a one-to-one relationship between product and categories will cause major code changes later if we need to start supporting multiple categories. Let's set up a category association that will allow us to start off tracking a single category per product, but easily support more later as we grow our features.
For now, categories will contain only textual information. Our first order of business is to decide where categories live in the application. We have our `Catalog` context, which manages the exhibition of our products. Product categorization is a natural fit here. Phoenix is also smart enough to generate code inside an existing context, which makes adding new resources to a context a breeze. Run the following command at your project root:
> Sometimes it may be tricky to determine if two resources belong to the same context or not. In those cases, prefer distinct contexts per resource and refactor later if necessary. Otherwise you can easily end up with large contexts of loosely related entities. Also keep in mind that the fact two resources are related does not necessarily mean they belong to the same context, otherwise you would quickly end up with one large context, as the majority of resources in an application are connected to each other. To sum it up: if you are unsure, you should prefer separate modules (contexts).
```console
mix phx.gen.context Catalog Category categories \
title:string:unique --no-scope
```
You will see the following output in your terminal:
```console
You are generating into an existing context.
The Hello.Catalog context currently has 7 functions and 1 file in its directory.
* It's OK to have multiple resources in the same context as long as they are closely related.
...
Would you like to proceed? [Yn]
```
Type `y` followed by the `Return` key.
You should see output similar to:
```console
* creating lib/hello/catalog/category.ex
* creating priv/repo/migrations/20250203192325_create_categories.exs
* injecting lib/hello/catalog.ex
* injecting test/hello/catalog_test.exs
* injecting test/support/fixtures/catalog_fixtures.ex
Remember to update your repository by running migrations:
$ mix ecto.migrate
```
This time around, we used `mix phx.gen.context`, which is just like `mix phx.gen.html`, except it doesn't generate the web files for us. Since we already have controllers and templates for managing products, we can integrate the new category features into our existing web form and product show page. We can see we now have a new `Category` schema alongside our product schema at `lib/hello/catalog/category.ex`, and Phoenix told us it was *injecting* new functions in our existing Catalog context for the category functionality. The injected functions will look very familiar to our product functions, with new functions like `create_category`, `list_categories`, and so on. Before we migrate up, we need to do a second bit of code generation. Our category schema is great for representing an individual category in the system, but we need to support a many-to-many relationship between products and categories. Fortunately, ecto allows us to do this simply with a join table, so let's generate that now with the `ecto.gen.migration` command:
```console
mix ecto.gen.migration create_product_categories
```
You will see output confirming the migration file was created:
```console
* creating priv/repo/migrations/20250203192958_create_product_categories.exs
```
Next, let's open up the new migration file and add the following code to the `change` function:
```elixir
defmodule Hello.Repo.Migrations.CreateProductCategories do
use Ecto.Migration
def change do
create table(:product_categories, primary_key: false) do
add :product_id, references(:products, on_delete: :delete_all)
add :category_id, references(:categories, on_delete: :delete_all)
end
create index(:product_categories, [:product_id])
create unique_index(:product_categories, [:category_id, :product_id])
end
end
```
We created a `product_categories` table and used the `primary_key: false` option since our join table does not need a primary key. Next we defined our `:product_id` and `:category_id` foreign key fields, and passed `on_delete: :delete_all` to ensure the database prunes our join table records if a linked product or category is deleted. By using a database constraint, we enforce data integrity at the database level, rather than relying on ad-hoc and error-prone application logic.
Next, we created indexes for our foreign keys, one of which is a unique index to ensure a product cannot have duplicate categories. Note that we do not necessarily need single-column index for `category_id` because it is in the leftmost prefix of multicolumn index, which is enough for the database optimizer. Adding a redundant index, on the other hand, only adds overhead on write.
With our migrations in place, we can migrate up.
```console
mix ecto.migrate
```
You will see the following output confirming migration success:
```
18:20:36.489 [info] == Running 20250222231834 Hello.Repo.Migrations.CreateCategories.change/0 forward
18:20:36.493 [info] create table categories
18:20:36.508 [info] create index categories_title_index
18:20:36.512 [info] == Migrated 20250222231834 in 0.0s
18:20:36.547 [info] == Running 20250222231930 Hello.Repo.Migrations.CreateProductCategories.change/0 forward
18:20:36.547 [info] create table product_categories
18:20:36.557 [info] create index product_categories_product_id_index
18:20:36.560 [info] create index product_categories_category_id_product_id_index
18:20:36.562 [info] == Migrated 20250222231930 in 0.0s
```
Now that we have a `Catalog.Product` schema and a join table to associate products and categories, we're nearly ready to start wiring up our new features. Before we dive in, we first need real categories to select in our web UI. Let's quickly seed some new categories in the application. Add the following code to your seeds file in `priv/repo/seeds.exs`:
```elixir
for title <- ["Home Improvement", "Power Tools", "Gardening", "Books", "Education"] do
{:ok, _} = Hello.Catalog.create_category(%{title: title})
end
```
We simply enumerate over a list of category titles and use the generated `create_category/1` function of our catalog context to persist the new records. We can run the seeds with `mix run`:
```console
mix run priv/repo/seeds.exs
```
The output in the terminal confirms the `seeds.exs` executed successfully:
```console
[debug] QUERY OK db=3.1ms decode=1.1ms queue=0.7ms idle=2.2ms
INSERT INTO "categories" ("title","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["Home Improvement", ~N[2025-02-03 19:39:53], ~N[2025-02-03 19:39:53]]
[debug] QUERY OK db=1.2ms queue=1.3ms idle=12.3ms
INSERT INTO "categories" ("title","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["Power Tools", ~N[2025-02-03 19:39:53], ~N[2025-02-03 19:39:53]]
[debug] QUERY OK db=1.1ms queue=1.1ms idle=15.1ms
INSERT INTO "categories" ("title","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["Gardening", ~N[2025-02-03 19:39:53], ~N[2025-02-03 19:39:53]]
[debug] QUERY OK db=2.4ms queue=1.0ms idle=17.6ms
INSERT INTO "categories" ("title","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["Books", ~N[2025-02-03 19:39:53], ~N[2025-02-03 19:39:53]]
```
Perfect. Before we integrate categories in the web layer, we need to let our context know how to associate products and categories. First, open up `lib/hello/catalog/product.ex` and add the following association:
```diff
+ alias Hello.Catalog.Category
schema "products" do
field :description, :string
field :price, :decimal
field :title, :string
field :views, :integer
+ many_to_many :categories, Category, join_through: "product_categories", on_replace: :delete
timestamps(type: :utc_datetime)
end
```
We used `Ecto.Schema`'s `many_to_many` macro to let Ecto know how to associate our product to multiple categories through the `"product_categories"` join table. We also used the `on_replace: :delete` option to declare that any existing join records should be deleted when we are changing our categories.
With our schema associations set up, we can implement the selection of categories in our product form. To do so, we need to translate the user input of catalog IDs from the front-end to our many-to-many association. Fortunately Ecto makes this a breeze now that our schema is set up. Open up your catalog context and make the following changes:
```diff
+ alias Hello.Catalog.Category
- def get_product!(id), do: Repo.get!(Product, id)
+ def get_product!(id) do
+ Product
+ |> Repo.get!(id)
+ |> Repo.preload(:categories)
+ end
def create_product(attrs) do
%Product{}
- |> Product.changeset(attrs)
+ |> change_product(attrs)
|> Repo.insert()
end
def update_product(%Product{} = product, attrs) do
product
- |> Product.changeset(attrs)
+ |> change_product(attrs)
|> Repo.update()
end
def change_product(%Product{} = product, attrs \\ %{}) do
- Product.changeset(product, attrs)
+ categories = list_categories_by_id(attrs["category_ids"])
+ product
+ |> Repo.preload(:categories)
+ |> Product.changeset(attrs)
+ |> Ecto.Changeset.put_assoc(:categories, categories)
end
+ def list_categories_by_id(nil), do: []
+ def list_categories_by_id(category_ids) do
+ Repo.all(from c in Category, where: c.id in ^category_ids)
+ end
```
First, we added `Repo.preload` to preload our categories when we fetch a product. This will allow us to reference `product.categories` in our controllers, templates, and anywhere else we want to make use of category information. Next, we modified our `create_product` and `update_product` functions to call into our existing `change_product` function to produce a changeset. Within `change_product` we added a lookup to find all categories if the `"category_ids"` attribute is present. Then we preloaded categories and called `Ecto.Changeset.put_assoc` to place the fetched categories into the changeset. Finally, we implemented the `list_categories_by_id/1` function to query the categories matching the category IDs, or return an empty list if no `"category_ids"` attribute is present. Now our `create_product` and `update_product` functions receive a changeset with the category associations all ready to go once we attempt an insert or update against our repo.
Next, let's expose our new feature to the web by adding the category input to our product form. To keep our form template tidy, let's write a new function to wrap up the details of rendering a category select input for our product. Open up your `ProductHTML` view in `lib/hello_web/controllers/product_html.ex` and key this in:
```elixir
def category_opts(changeset) do
existing_ids =
changeset
|> Ecto.Changeset.get_change(:categories, [])
|> Enum.map(& &1.data.id)
for cat <- Hello.Catalog.list_categories() do
[key: cat.title, value: cat.id, selected: cat.id in existing_ids]
end
end
```
We added a new `category_opts/1` function which generates the select options for a multiple select tag we will add soon. We calculated the existing category IDs from our changeset, then used those values when we generate the select options for the input tag. We did this by enumerating over all of our categories and returning the appropriate `key`, `value`, and `selected` values. We marked an option as selected if the category ID was found in those category IDs in our changeset.
With our `category_opts` function in place, we can open up `lib/hello_web/controllers/product_html/product_form.html.heex` and add:
```diff
...
<.input field={f[:views]} type="number" label="Views" />
+ <.input field={f[:category_ids]} type="select" multiple options={category_opts(@changeset)} />
<.button>Save Product
```
We added a `category_select` above our save button. Now let's try it out. Next, let's show the product's categories in the product show template. Add the following code to the list in `lib/hello_web/controllers/product_html/show.html.heex`:
```diff
<.list>
...
+ <:item title="Categories">
+
+
{cat.title}
+
+
```
Now if we start the server with `mix phx.server` and visit [http://localhost:4000/products/new](http://localhost:4000/products/new), we'll see the new category multiple select input. Enter some valid product details, select a category or two, and click save.
```text
Title: Elixir Flashcards
Description: Flash card set for the Elixir programming language
Price: 5.000000
Views: 0
Categories:
Education
Books
```
It's not much to look at yet, but it works! We added relationships within our context complete with data integrity enforced by the database. Not bad. Let's keep building!
================================================
FILE: guides/data_modelling/more_examples.md
================================================
# 5. Bringing It Home
With our `Catalog` and `ShoppingCart` contexts, we're seeing first-hand how our well-considered modules and function names are yielding clear and maintainable code. Our last order of business is to allow the user to initiate the checkout process. We won't go as far as integrating payment processing or order fulfillment, but we'll get you started in that direction. This will be a great opportunity to put what we have learned so far in practice.
Like before, we need to decide where code for completing an order should live. Is it part of the catalog? Clearly not, but what about the shopping cart? Shopping carts are related to orders – after all, the user has to add items in order to purchase any products – but should the order checkout process be grouped here?
If we stop and consider the order process, we'll see that orders involve related, but distinctly different data from the cart contents. Also, business rules around the checkout process are much different than carting. For example, we may allow a user to add a back-ordered item to their cart, but we could not allow an order with no inventory to be completed. Additionally, we need to capture point-in-time product information when an order is completed, such as the price of the items *at payment transaction time*. This is essential because a product price may change in the future, but the line items in our order must always record and display what we charged at time of purchase. For these reasons, we can start to see that ordering can stand on its own with its own data concerns and business rules.
Naming wise, `Orders` clearly defines our context, so let's get started by again taking advantage of the context generators. Note that the `user` scope generated by `mix phx.gen.auth` is marked as default scope (in your `config/config.exs`), therefore we don't need to specify it in our command. There can be different scopes in an application, in which case the `--scope` option can be used when running the generators. Run the following command in your console:
```console
$ mix phx.gen.context Orders Order orders total_price:decimal
* creating lib/hello/orders/order.ex
* creating priv/repo/migrations/20250209214612_create_orders.exs
* creating lib/hello/orders.ex
* injecting lib/hello/orders.ex
* creating test/hello/orders_test.exs
* injecting test/hello/orders_test.exs
* creating test/support/fixtures/orders_fixtures.ex
* injecting test/support/fixtures/orders_fixtures.ex
Remember to update your repository by running migrations:
$ mix ecto.migrate
```
We generated an `Orders` context. The order is automatically scoped to the current user and added a `total_price` column. With our starting point in place, let's open up the newly created migration in `priv/repo/migrations/*_create_orders.exs` and make the following changes:
```diff
def change do
create table(:orders) do
- add :total_price, :decimal
+ add :total_price, :decimal, precision: 15, scale: 6, null: false
add :user_id, references(:users, type: :id, on_delete: :delete_all)
timestamps(type: :utc_datetime)
end
end
```
Like we did previously, we gave appropriate precision and scale options for our decimal column which will allow us to store currency without precision loss. We also added a not-null constraint to enforce all orders to have a price.
The orders table alone doesn't hold much information, but we know we'll need to store point-in-time product price information of all the items in the order. For that, we'll add an additional struct for this context named `LineItem`. Line items will capture the price of the product *at payment transaction time*. Please run the following command:
```console
$ mix phx.gen.context Orders LineItem order_line_items \
price:decimal quantity:integer \
order_id:references:orders product_id:references:products --no-scope
You are generating into an existing context.
...
Would you like to proceed? [Yn] y
* creating lib/hello/orders/line_item.ex
* creating priv/repo/migrations/20250209215050_create_order_line_items.exs
* injecting lib/hello/orders.ex
* injecting test/hello/orders_test.exs
* injecting test/support/fixtures/orders_fixtures.ex
Remember to update your repository by running migrations:
$ mix ecto.migrate
```
We used the `phx.gen.context` command to generate the `LineItem` Ecto schema and inject supporting functions into our orders context. Like before, let's modify the migration in `priv/repo/migrations/*_create_order_line_items.exs` and make the following decimal field changes:
```diff
def change do
create table(:order_line_items) do
- add :price, :decimal
+ add :price, :decimal, precision: 15, scale: 6, null: false
add :quantity, :integer
add :order_id, references(:orders, on_delete: :nothing)
add :product_id, references(:products, on_delete: :nothing)
timestamps(type: :utc_datetime)
end
create index(:order_line_items, [:order_id])
create index(:order_line_items, [:product_id])
end
```
With our migration in place, let's wire up our orders and line items associations in `lib/hello/orders/order.ex`:
```diff
schema "orders" do
field :total_price, :decimal
- field :user_id, :id
+ belongs_to :user, Hello.Accounts.User
+ has_many :line_items, Hello.Orders.LineItem
+ has_many :products, through: [:line_items, :product]
timestamps(type: :utc_datetime)
end
```
We used `has_many :line_items` to associate orders and line items, just like we've seen before. Next, we used the `:through` feature of `has_many`, which allows us to instruct ecto how to associate resources across another relationship. In this case, we can associate products of an order by finding all products through associated line items. Next, let's wire up the association in the other direction in `lib/hello/orders/line_item.ex`:
```diff
schema "order_line_items" do
field :price, :decimal
field :quantity, :integer
- field :order_id, :id
- field :product_id, :id
+ belongs_to :order, Hello.Orders.Order
+ belongs_to :product, Hello.Catalog.Product
timestamps(type: :utc_datetime)
end
```
We used `belongs_to` to associate line items to orders and products. With our associations in place, we can start integrating the web interface into our order process. Open up your router `lib/hello_web/router.ex` and add the following line:
```diff
scope "/", HelloWeb do
pipe_through [:browser, :require_authenticated_user]
resources "/cart_items", CartItemController, only: [:create, :delete]
get "/cart", CartController, :show
put "/cart", CartController, :update
+ resources "/orders", OrderController, only: [:create, :show]
end
```
We wired up `create` and `show` routes for our generated `OrderController`, since these are the only actions we need at the moment. With our routes in place, we can now migrate up:
```console
$ mix ecto.migrate
17:14:37.715 [info] == Running 20250209214612 Hello.Repo.Migrations.CreateOrders.change/0 forward
17:14:37.720 [info] create table orders
17:14:37.755 [info] == Migrated 20250209214612 in 0.0s
17:14:37.784 [info] == Running 20250209215050 Hello.Repo.Migrations.CreateOrderLineItems.change/0 forward
17:14:37.785 [info] create table order_line_items
17:14:37.795 [info] create index order_line_items_order_id_index
17:14:37.796 [info] create index order_line_items_product_id_index
17:14:37.798 [info] == Migrated 20250209215050 in 0.0s
```
Before we render information about our orders, we need to ensure our order data is fully populated and can be looked up by a current user. Open up your orders context in `lib/hello/orders.ex` and adjust your `get_order!/2` to include a preload:
```diff
def get_order!(%Scope{} = scope, id) do
- Repo.get_by!(Order, id: id, user_id: scope.user.id)
+ Order
+ |> Repo.get_by!(id: id, user_id: scope.user.id)
+ |> Repo.preload([line_items: [:product]])
end
```
To complete an order, our cart page can issue a POST to the `OrderController.create` action, but we need to implement the operations and logic to actually complete an order. Like before, we'll start at the web interface. Create a new file at `lib/hello_web/controllers/order_controller.ex` and key this in:
```elixir
defmodule HelloWeb.OrderController do
use HelloWeb, :controller
alias Hello.Orders
def create(conn, _) do
case Orders.complete_order(conn.assigns.current_scope, conn.assigns.cart) do
{:ok, order} ->
conn
|> put_flash(:info, "Order created successfully.")
|> redirect(to: ~p"/orders/#{order}")
{:error, _reason} ->
conn
|> put_flash(:error, "There was an error processing your order")
|> redirect(to: ~p"/cart")
end
end
end
```
We wrote the `create` action to call an as-yet-implemented `Orders.complete_order/2` function. Our code is technically "creating" an order, but it's important to step back and consider the naming of your interfaces. The act of *completing* an order is extremely important in our system. Money changes hands in a transaction, physical goods could be automatically shipped, etc. Such an operation deserves a better, more obvious function name, such as `complete_order`. If the order is completed successfully we redirect to the show page, otherwise a flash error is shown as we redirect back to the cart page.
Here is also a good opportunity to highlight that contexts can naturally work with data defined by other contexts too. This will be especially common with data that is used throughout the application, such as the cart here (but it can also be the current user or the current project, and so forth, depending on your project).
Now we can implement our `Orders.complete_order/2` function. To complete an order, our job will require a few operations:
1. A new order record must be persisted with the total price of the order
2. All items in the cart must be transformed into new order line items records
with quantity and point-in-time product price information
3. After successful order insert (and eventual payment), items must be pruned
from the cart
From our requirements alone, we can start to see why a generic `create_order` function doesn't cut it. Let's implement this new function in `lib/hello/orders.ex`:
```elixir
alias Hello.Orders.LineItem
alias Hello.ShoppingCart
def complete_order(%Scope{} = scope, %ShoppingCart.Cart{} = cart) do
true = cart.user_id == scope.user.id
line_items =
Enum.map(cart.items, fn item ->
%{
product_id: item.product_id,
price: item.product.price,
quantity: item.quantity
}
end)
order_changeset =
Ecto.Changeset.change(%Order{}, %{
user_id: scope.user.id,
total_price: ShoppingCart.total_cart_price(cart),
line_items: line_items
})
Repo.transact(fn ->
with {:ok, order} <- Repo.insert(order_changeset),
{:ok, _cart} <- ShoppingCart.prune_cart_items(scope, cart) do
{:ok, order}
end
end)
|> case do
{:ok, order} ->
broadcast_order(scope, {:created, order})
{:ok, order}
{:error, reason} ->
{:error, reason}
end
end
```
We started by mapping the `%ShoppingCart.CartItem{}`'s in our shopping cart into a map of order line items structs. The job of the order line item record is to capture the price of the product *at payment transaction time*, so we reference the product's price here. Next, we create a bare order changeset with `Ecto.Changeset.change/2` and associate our user UUID, set our total price calculation, and place our order line items in the changeset. With a fresh order changeset ready to be inserted, we now make use of `Repo.transact/2` to execute our operations in a database transaction. We start by inserting the order, followed by a step that prunes all items from the user’s cart. The function wrapped inside `Repo.transact/2` must either return `{:ok, result}` or error, which halts and rolls back the transaction. Running the transaction will execute these operations in sequence, and we return the result to the caller once completed.
To close out our order completion, we need to implement the `ShoppingCart.prune_cart_items/1` function in `lib/hello/shopping_cart.ex`:
```elixir
def prune_cart_items(%Scope{} = scope, %Cart{} = cart) do
{_, _} = Repo.delete_all(from(i in CartItem, where: i.cart_id == ^cart.id))
{:ok, get_cart(scope)}
end
```
Our new function accepts the cart struct and issues a `Repo.delete_all` which accepts a query of all items for the provided cart. We return a success result by simply reloading the pruned cart to the caller. With our context complete, we now need to show the user their completed order. Head back to your order controller and add the `show/2` action:
```elixir
def show(conn, %{"id" => id}) do
order = Orders.get_order!(conn.assigns.current_scope, id)
render(conn, :show, order: order)
end
```
We added the show action to pass our `conn.assigns.current_scope` to `get_order!` which authorizes orders to be viewable only by the owner of the order. Next, we can implement the view and template. Create a new view file at `lib/hello_web/controllers/order_html.ex` with the following content:
```elixir
defmodule HelloWeb.OrderHTML do
use HelloWeb, :html
embed_templates "order_html/*"
end
```
Next we can create the template at `lib/hello_web/controllers/order_html/show.html.heex`:
```heex
<.header>
Thank you for your order!
<:subtitle>
Email: {@current_scope.user.email}
<.table id="items" rows={@order.line_items}>
<:col :let={item} label="Title">{item.product.title}
<:col :let={item} label="Quantity">{item.quantity}
<:col :let={item} label="Price">
{HelloWeb.CartHTML.currency_to_str(item.price)}
Total price:
{HelloWeb.CartHTML.currency_to_str(@order.total_price)}
<.button navigate={~p"/products"}>Back to products
```
To show our completed order, we displayed the order's user, followed by the line item listing with product title, quantity, and the price we "transacted" when completing the order, along with the total price.
Our last addition will be to add the "complete order" button to our cart page to allow completing an order. Add the following button to the <.header> of the cart show template in `lib/hello_web/controllers/cart_html/show.html.heex`:
```diff
<.header>
My Cart
+ <:actions>
+ <.button href={~p"/orders"} method="post">
+ Complete order
+
+
```
We added a link with `method="post"` to send a POST request to our `OrderController.create` action. If we head back to our cart page at [`http://localhost:4000/cart`](http://localhost:4000/cart) and complete an order, we'll be greeted by our rendered template:
```text
Thank you for your order!
User uuid: 08964c7c-908c-4a55-bcd3-9811ad8b0b9d
Title Quantity Price
Metaprogramming Elixir 2 $15.00
Total price: $30.00
```
We haven't added payments, but we can already see how our `ShoppingCart` and `Orders` context splitting is driving us towards a maintainable solution. With our cart items separated from our order line items, we are well equipped in the future to add payment transactions, cart price detection, and more.
Great work!
================================================
FILE: guides/data_modelling/your_first_context.md
================================================
# 2. Your First Context
An ecommerce platform has wide-reaching coupling across a codebase so it's important to think about writing well-defined modules. With that in mind, our goal is to build a product catalog API that handles creating, updating, and deleting the products available in our system. We'll start off with the basic features of showcasing our products, and we will add shopping cart features later. We'll see how starting with a solid foundation with isolated boundaries allows us to grow our application naturally as we add functionality.
Phoenix includes the `mix phx.gen.html`, `mix phx.gen.json`, `mix phx.gen.live`, and `mix phx.gen.context` generators that apply the ideas of isolating functionality in our applications into contexts. These generators are a great way to hit the ground running while Phoenix nudges you in the right direction to grow your application. Let's put these tools to use for our new product catalog context.
When we run the generators, the context name is optional, and Phoenix will automatically use the plural name as the context module. This allows us to keep moving forward when starting out or when it is not yet clear how the different parts of our system relate to each other. Luckily, the needs of ecommerce systems are well defined nowadays, so it provides an excellent ground for us to design with intent. So let's take a step back and think about the different parts of our system.
We know that we'll have products to showcase on pages for sale, along with descriptions, pricing, etc. Along with selling products, we know we'll need to support carting, order checkout, and so on. While the products being purchased are related to the cart and checkout processes, showcasing a product and managing the *exhibition* of our products is distinctly different than tracking what a user has placed in their cart or how an order is placed. A `Catalog` context is a natural place for the management of our product details and the showcasing of those products we have for sale.
## Starting with generators
To jump-start our catalog context, we'll use `mix phx.gen.html` which creates a context module that wraps up Ecto access for creating, updating, and deleting products, along with web files like controllers and templates for the web interface into our context. Run the following command at your project root:
```console
$ mix phx.gen.html Catalog Product products title:string \
description:string price:decimal views:integer
```
After executing the command, you should see output similar to the following:
```console
* creating lib/hello_web/controllers/product_controller.ex
* creating lib/hello_web/controllers/product_html/edit.html.heex
* creating lib/hello_web/controllers/product_html/index.html.heex
* creating lib/hello_web/controllers/product_html/new.html.heex
* creating lib/hello_web/controllers/product_html/show.html.heex
* creating lib/hello_web/controllers/product_html/product_form.html.heex
* creating lib/hello_web/controllers/product_html.ex
* creating test/hello_web/controllers/product_controller_test.exs
* creating lib/hello/catalog/product.ex
* creating priv/repo/migrations/20250201185747_create_products.exs
* creating lib/hello/catalog.ex
* injecting lib/hello/catalog.ex
* creating test/hello/catalog_test.exs
* injecting test/hello/catalog_test.exs
* creating test/support/fixtures/catalog_fixtures.ex
* injecting test/support/fixtures/catalog_fixtures.ex
Add the resource to your browser scope in lib/hello_web/router.ex:
resources "/products", ProductController
Remember to update your repository by running migrations:
$ mix ecto.migrate
```
Phoenix generated the web files as expected in `lib/hello_web/`. We can also see our context functions were generated inside a `lib/hello/catalog.ex` file and our product schema file is placed in the directory of the same name. Note the difference between `lib/hello` and `lib/hello_web`. We have a `Catalog` module to serve as the public API for product catalog functionality, as well as a `Catalog.Product` struct, which is an Ecto schema for casting and validating product data. Phoenix also provided web and context tests for us, it also included test helpers for creating entities via the `Hello.Catalog` context, which we'll look at later. For now, let's follow the instructions and add the route according to the console instructions, in `lib/hello_web/router.ex`:
```diff
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :index
+ resources "/products", ProductController
end
```
With the new route in place, Phoenix reminds us to update our repo by running `mix ecto.migrate`, but first we need to make a few tweaks to the generated migration in `priv/repo/migrations/*_create_products.exs`:
```diff
def change do
create table(:products) do
add :title, :string
add :description, :string
- add :price, :decimal
+ add :price, :decimal, precision: 15, scale: 6, null: false
- add :views, :integer
+ add :views, :integer, default: 0, null: false
timestamps(type: :utc_datetime)
end
```
We modified our price column to a specific precision of 15, scale of 6, along with a not-null constraint. This ensures we store currency with proper precision for any mathematical operations we may perform. Next, we added a default value and not-null constraint to our views count. With our changes in place, we're ready to migrate up our database. Let's do that now:
```console
$ mix ecto.migrate
14:09:02.260 [info] == Running 20250201185747 Hello.Repo.Migrations.CreateProducts.change/0 forward
14:09:02.262 [info] create table products
14:09:02.273 [info] == Migrated 20250201185747 in 0.0s
```
Before we jump into the generated code, let's start the server with `mix phx.server` and visit [http://localhost:4000/products](http://localhost:4000/products). Let's follow the "New Product" link and click the "Save" button without providing any input. When we submit the form, we can see all the validation errors inline with the inputs. Nice! Out of the box, the context generator included the schema fields in our form template and we can see our default validations for required inputs are in effect. Let's enter some example product data and resubmit the form:
```text
Product created successfully.
Title: Metaprogramming Elixir
Description: Write Less Code, Get More Done (and Have Fun!)
Price: 15.000000
Views: 0
```
If we follow the "Back" link, we get a list of all products, which should contain the one we just created. Likewise, we can update this record or delete it. Now that we've seen how it works in the browser, it's time to take a look at the generated code.
> #### Naming things is hard {: .tip}
>
> When starting a web application, it may be hard to draw lines or name its different contexts, especially when the business domain you are working with is not as well established as ecommerce.
>
> For those reasons, Phoenix generators allow you to skip the context name, which is really helpful when you're stuck or still exploring your business domain. For example, our code above would work the same if we used the default `Products` context for managing products and it would still allow us to organically discover other resources that belong to the `Products` context, such as categories or image galleries.
>
> We also advise against being too smart when naming your contexts. Pick a name that is clear and obvious to everyone who works (and might work) in the project. As your application grows and the different parts of your system become clear, you can simply rename the context or move resources around. The beauty of Elixir modules is moving them around should be simply a matter of renaming the module names and their callers (and renaming the files for consistency).
## Grokking generated code
That little `mix phx.gen.html` command packed a surprising punch. We got a lot of functionality out-of-the-box for creating, updating, and deleting products in our catalog. This is far from a full-featured app, but remember, generators are first and foremost learning tools and a starting point for you to begin building real features. Code generation can't solve all your problems, but it will teach you the ins and outs of Phoenix and nudge you towards the proper mindset when designing your application.
Let's first check out the `ProductController` that was generated in `lib/hello_web/controllers/product_controller.ex`:
```elixir
defmodule HelloWeb.ProductController do
use HelloWeb, :controller
alias Hello.Catalog
alias Hello.Catalog.Product
def index(conn, _params) do
products = Catalog.list_products()
render(conn, :index, products: products)
end
def new(conn, _params) do
changeset = Catalog.change_product(%Product{})
render(conn, :new, changeset: changeset)
end
def create(conn, %{"product" => product_params}) do
case Catalog.create_product(product_params) do
{:ok, product} ->
conn
|> put_flash(:info, "Product created successfully.")
|> redirect(to: ~p"/products/#{product}")
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, :new, changeset: changeset)
end
end
def show(conn, %{"id" => id}) do
product = Catalog.get_product!(id)
render(conn, :show, product: product)
end
...
end
```
We've seen how controllers work in our [controller guide](controllers.html), so the code probably isn't too surprising. What is worth noticing is how our controller calls into the `Catalog` context. We can see that the `index` action fetches a list of products with `Catalog.list_products/0`, and how products are persisted in the `create` action with `Catalog.create_product/1`. We haven't yet looked at the catalog context, so we don't yet know how product fetching and creation is happening under the hood – *but that's the point*. Our Phoenix controller is the web interface into our greater application. It shouldn't be concerned with the details of how products are fetched from the database or persisted into storage. We only care about telling our application to perform some work for us. This is great because our business logic and storage details are decoupled from the web layer of our application. If we move to a full-text storage engine later for fetching products instead of a SQL query, our controller doesn't need to be changed. Likewise, we can reuse our context code from any other interface in our application, be it a channel, mix task, or long-running process importing CSV data.
In the case of our `create` action, when we successfully create a product, we use `Phoenix.Controller.put_flash/3` to show a success message, and then we redirect to the router's product show page. Conversely, if `Catalog.create_product/1` fails, we render our `"new.html"` template and pass along the Ecto changeset for the template to lift error messages from.
Next, let's dig deeper and check out our `Catalog` context in `lib/hello/catalog.ex`:
```elixir
defmodule Hello.Catalog do
@moduledoc """
The Catalog context.
"""
import Ecto.Query, warn: false
alias Hello.Repo
alias Hello.Catalog.Product
@doc """
Returns the list of products.
## Examples
iex> list_products()
[%Product{}, ...]
"""
def list_products do
Repo.all(Product)
end
...
end
```
This module will be the public API for all product catalog functionality in our system. For example, in addition to product detail management, we may also handle product category classification and product variants for things like optional sizing, trims, etc. If we look at the `list_products/0` function, we can see the private details of product fetching. And it's super simple. We have a call to `Repo.all(Product)`. We saw how Ecto repo queries worked in the [Ecto guide](ecto.html), so this call should look familiar. Our `list_products` function is a generalized function name specifying the *intent* of our code – namely to list products. The details of that intent where we use our Repo to fetch the products from our PostgreSQL database, are hidden from our callers. This is a common theme we'll see reiterated as we use the Phoenix generators. Phoenix will push us to think about where we have different responsibilities in our application, and then to wrap up those different areas behind well-named modules and functions that make the intent of our code clear, while encapsulating the details.
Now we know how data is fetched, but how are products persisted? Let's take a look at the `Catalog.create_product/1` function:
```elixir
@doc """
Creates a product.
## Examples
iex> create_product(%{field: value})
{:ok, %Product{}}
iex> create_product(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_product(attrs) do
%Product{}
|> Product.changeset(attrs)
|> Repo.insert()
end
```
There's more documentation than code here, but a couple of things are important to highlight. First, we can see again that our Ecto Repo is used under the hood for database access. You probably also noticed the call to `Product.changeset/2`. We talked about changesets before, and now we see them in action in our context.
If we open up the `Product` schema in `lib/hello/catalog/product.ex`, it will look immediately familiar:
```elixir
defmodule Hello.Catalog.Product do
use Ecto.Schema
import Ecto.Changeset
schema "products" do
field :description, :string
field :price, :decimal
field :title, :string
field :views, :integer
timestamps(type: :utc_datetime)
end
@doc false
def changeset(product, attrs) do
product
|> cast(attrs, [:title, :description, :price, :views])
|> validate_required([:title, :description, :price, :views])
end
end
```
This is just what we saw before when we ran `mix phx.gen.schema`, except here we see a `@doc false` above our `changeset/2` function. This tells us that while this function is publicly callable, it's not part of the public context API. Callers that build changesets do so via the context API. For example, `Catalog.create_product/1` calls into our `Product.changeset/2` to build the changeset from user input. Callers, such as our controller actions, do not access `Product.changeset/2` directly. All interaction with our product changesets is done through the public `Catalog` context.
## Adding Catalog functions
As we've seen, your context modules are dedicated modules that expose and group related functionality. Phoenix generates generic functions, such as `list_products` and `update_product`, but they only serve as a basis for you to grow your business logic and application from. Let's add one of the basic features of our catalog by tracking product page view count.
For any ecommerce system, the ability to track how many times a product page has been viewed is essential for marketing, suggestions, ranking, etc. While we could try to use the existing `Catalog.update_product` function, along the lines of `Catalog.update_product(product, %{views: product.views + 1})`, this would not only be prone to race conditions, but it would also require the caller to know too much about our Catalog system. To see why the race condition exists, let's walk through the possible execution of events:
Intuitively, you would assume the following events:
1. User 1 loads the product page with count of 13
2. User 1 saves the product page with count of 14
3. User 2 loads the product page with count of 14
4. User 2 saves the product page with count of 15
While in practice this would happen:
1. User 1 loads the product page with count of 13
2. User 2 loads the product page with count of 13
3. User 1 saves the product page with count of 14
4. User 2 saves the product page with count of 14
The race conditions would make this an unreliable way to update the existing table since multiple callers may be updating out of date view values. There's a better way.
Let's think of a function that describes what we want to accomplish. Here's how we would like to use it:
```elixir
product = Catalog.inc_page_views(product)
```
That looks great. Our callers will have no confusion over what this function does, and we can wrap up the increment in an atomic operation to prevent race conditions.
Open up your catalog context (`lib/hello/catalog.ex`), and add this new function:
```elixir
def inc_page_views(%Product{} = product) do
{1, [%Product{views: views}]} =
from(p in Product, where: p.id == ^product.id, select: [:views])
|> Repo.update_all(inc: [views: 1])
put_in(product.views, views)
end
```
We built a query for fetching the current product given its ID which we pass to `Repo.update_all`. Ecto's `Repo.update_all` allows us to perform batch updates against the database, and is perfect for atomically updating values, such as incrementing our views count. The result of the repo operation returns the number of updated records, along with the selected schema values specified by the `select` option. When we receive the new product views, we use `put_in(product.views, views)` to place the new view count within the product struct.
With our context function in place, let's make use of it in our product controller. Update your `show` action in `lib/hello_web/controllers/product_controller.ex` to call our new function:
```elixir
def show(conn, %{"id" => id}) do
product =
id
|> Catalog.get_product!()
|> Catalog.inc_page_views()
render(conn, :show, product: product)
end
```
We modified our `show` action to pipe our fetched product into `Catalog.inc_page_views/1`, which will return the updated product. Then we rendered our template just as before. Let's try it out. Refresh one of your product pages a few times and watch the view count increase.
We can also see our atomic update in action in the ecto debug logs:
```text
[debug] QUERY OK source="products" db=0.5ms idle=834.5ms
UPDATE "products" AS p0 SET "views" = p0."views" + $1 WHERE (p0."id" = $2) RETURNING p0."views" [1, 1]
```
Good work!
As we've seen, designing with contexts gives you a solid foundation to grow your application from. Using discrete, well-defined APIs that expose the intent of your system allows you to write more maintainable applications with reusable code. Now that we know how to start extending our context API, let's explore handling relationships within a context.
================================================
FILE: guides/deployment/deployment.md
================================================
# Introduction to Deployment
Once we have a working application, we're ready to deploy it. If you're not quite finished with your own application, don't worry. Just follow the [Up and Running Guide](up_and_running.html) to create a basic application to work with.
When preparing an application for deployment, there are three main steps:
* Handling of your application secrets
* Compiling your application assets
* Starting your server in production
In this guide, we will learn how to get the production environment running locally. You can use the same techniques in this guide to run your application in production, but depending on your deployment infrastructure, extra steps will be necessary.
As an example of deploying to other infrastructures, we also discuss four different approaches in our guides: using [Elixir's releases](releases.html) with `mix release`, [using Gigalixir](gigalixir.html), [using Fly](fly.html), and [using Heroku](heroku.html). We've also included links to deploying Phoenix on other platforms under [Community Deployment Guides](#community-deployment-guides). Finally, the release guide has a sample Dockerfile you can use if you prefer to deploy with container technologies.
Let's explore those steps above one by one.
## Handling of your application secrets
All Phoenix applications have data that must be kept secure, for example, the username and password for your production database, and the secret Phoenix uses to sign and encrypt important information. The general recommendation is to keep those in environment variables and load them into your application. This is done in `config/runtime.exs` (formerly `config/prod.secret.exs` or `config/releases.exs`), which is responsible for loading secrets and configuration from environment variables at boot time.
Therefore, you need to make sure the proper relevant variables are set in production:
```console
$ mix phx.gen.secret
REALLY_LONG_SECRET
$ export SECRET_KEY_BASE=REALLY_LONG_SECRET
$ export DATABASE_URL=ecto://USER:PASS@HOST/database
```
Do not copy those values directly, set `SECRET_KEY_BASE` according to the result of `mix phx.gen.secret` and `DATABASE_URL` according to your database address.
If for some reason you do not want to rely on environment variables, you can hard code the secrets in your `config/runtime.exs` but make sure not to check the file into your version control system.
With your secret information properly secured, it is time to configure assets!
Before taking this step, we need to do one bit of preparation. Since we will be readying everything for production, we need to do some setup in that environment by getting our dependencies and compiling.
```console
$ mix deps.get --only prod
$ MIX_ENV=prod mix compile
```
## Compiling your application assets
This step is required only if you have compilable assets like JavaScript and stylesheets. By default, Phoenix uses `esbuild` but everything is encapsulated in a single `mix assets.deploy` task defined in your `mix.exs`:
```console
$ MIX_ENV=prod mix assets.deploy
Check your digested files at "priv/static".
```
And that is it! The Mix task by default builds the assets and then generates digests with a cache manifest file so Phoenix can quickly serve assets in production.
> Note: if you run the task above in your local machine, it will generate many digested assets in `priv/static`. You can prune them by running `mix phx.digest.clean --all`.
Keep in mind that, if you by any chance forget to run the steps above, Phoenix will show an error message:
```console
$ PORT=4001 MIX_ENV=prod mix phx.server
10:50:18.732 [info] Running MyAppWeb.Endpoint with Cowboy on http://example.com
10:50:18.735 [error] Could not find static manifest at "my_app/_build/prod/lib/foo/priv/static/cache_manifest.json". Run "mix phx.digest" after building your static files or remove the configuration from "config/prod.exs".
```
The error message is quite clear: it says Phoenix could not find a static manifest. Just run the commands above to fix it or, if you are not serving or don't care about assets at all, you can just remove the `cache_static_manifest` configuration from your config.
## Starting your server in production
To run Phoenix in production, we need to set the `PORT` and `MIX_ENV` environment variables when invoking `mix phx.server`:
```console
$ PORT=4001 MIX_ENV=prod mix phx.server
10:59:19.136 [info] Running MyAppWeb.Endpoint with Cowboy on http://example.com
```
To run in detached mode so that the Phoenix server does not stop and continues to run even if you close the terminal:
```console
$ PORT=4001 MIX_ENV=prod elixir --erl "-detached" -S mix phx.server
```
In case you get an error message, please read it carefully, and open up a bug report if it is still not clear how to address it.
You can also run your application inside an interactive shell:
```console
$ PORT=4001 MIX_ENV=prod iex -S mix phx.server
10:59:19.136 [info] Running MyAppWeb.Endpoint with Cowboy on http://example.com
```
## Putting it all together
The previous sections give an overview about the main steps required to deploy your Phoenix application. In practice, you will end-up adding steps of your own as well. For example, if you are using a database, you will also want to run `mix ecto.migrate` before starting the server to ensure your database is up to date.
Overall, here is a script you can use as a starting point:
```console
# Initial setup
$ mix deps.get --only prod
$ MIX_ENV=prod mix compile
# Compile assets
$ MIX_ENV=prod mix assets.deploy
# Custom tasks (like DB migrations)
$ MIX_ENV=prod mix ecto.migrate
# Finally run the server
$ PORT=4001 MIX_ENV=prod mix phx.server
```
And that's it. Next, you can use one of our official guides to deploy:
* [with Elixir's releases](releases.html)
* [to Gigalixir](gigalixir.html), an Elixir-centric Platform as a Service (PaaS)
* [to Fly.io](fly.html), a PaaS that deploys your servers close to your users with built-in distribution support
* and [to Heroku](heroku.html), one of the most popular PaaS.
## Clustering and Long-Polling Transports
Phoenix supports two types of transports for its Socket implementation: WebSocket, and Long-Polling. When generating a Phoenix project, you can see the default configuration set in the generated `endpoint.ex` file:
```elixir
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [session: @session_options]],
longpoll: [connect_info: [session: @session_options]]
```
This configuration tells Phoenix that both the WebSocket and the Long-Polling options are available, and based on the client's network conditions, Phoenix will first attempt to connect to the WebSocket, falling back to the Long-Poll option after the configured timeout found in the generated `app.js` file:
```javascript
let liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,
params: {_csrf_token: csrfToken}
})
```
If you are running more than one machine in production, which is the recommended approach in most cases, this automatic fallback comes with an important caveat. If you want Long-Polling to work properly, your application must either:
1. Utilize the Erlang VM's clustering capabilities, so the default `Phoenix.PubSub` adapter can broadcast messages across nodes
2. Choose a different `Phoenix.PubSub` adapter (such as `Phoenix.PubSub.Redis`)
3. Or your deployment option must implement sticky sessions - ensuring that all requests for a specific session go to the same machine
The reason for this is simple. While a WebSocket is a long-lived open connection to the same machine, long-polling works by opening a request to the server, waiting for a timeout or until the open request is fulfilled, and repeating this process. In order to preserve the state of the user's connected socket and to preserve the behaviour of a socket being long-lived, the user's process is kept alive, and each long-poll request attempts to find the user's stateful process. If the stateful process is not reachable, every request will create a new process and a new state, thereby breaking the fact that the socket is long-lived and stateful.
## Community Deployment Guides
* [Render](https://render.com) has first class support for Phoenix applications. There are guides for hosting Phoenix with [Mix releases](https://render.com/docs/deploy-phoenix) and as a [Distributed Elixir Cluster](https://render.com/docs/deploy-elixir-cluster).
* [Railway](https://railway.com) also provides support for Phoenix applications using [Mix releases](https://docs.railway.com/guides/phoenix).
================================================
FILE: guides/deployment/fly.md
================================================
# Deploying on Fly.io
The main goal for this guide is to get a Phoenix application running on [Fly.io](https://fly.io).
Fly.io maintains their own guide for Elixir/Phoenix here: [Fly.io/docs/elixir/getting-started/](https://fly.io/docs/elixir/getting-started/). We will keep this guide up but for the latest and greatest check with them!
## What we'll need
The only thing we'll need for this guide is a working Phoenix application. For those of us who need a simple application to deploy, please follow the [Up and Running guide](https://hexdocs.pm/phoenix/up_and_running.html).
You can just:
```console
$ mix phx.new my_app
```
## Sections
Let's separate this process into a few steps, so we can keep track of where we are.
- Install the Fly.io CLI
- Sign up for Fly.io
- Deploy the app to Fly.io
- Clustering your application
- Extra Fly.io tips
- Helpful Fly.io resources
## Installing the Fly.io CLI
Follow the instructions [here](https://fly.io/docs/getting-started/installing-flyctl/) to install Flyctl, the command-line interface for the Fly.io platform.
## Sign up for Fly.io
We can [sign up for an account](https://fly.io/docs/getting-started/log-in-to-fly/) using the CLI.
```console
$ fly auth signup
```
Or sign in.
```console
$ fly auth login
```
Fly has a [free tier](https://fly.io/docs/about/pricing/) for most applications. A credit card is required when setting up an account to help prevent abuse. See the [pricing](https://fly.io/docs/about/pricing/) page for more details.
## Deploy the app to Fly.io
To tell Fly about your application, run `fly launch` in the directory with your source code. This creates and configures a Fly.io app.
```console
$ fly launch
```
This scans your source, detects the Phoenix project, and runs `mix phx.gen.release --docker` for you! This creates a Dockerfile for you.
The `fly launch` command walks you through a few questions.
- You can name the app or have it generate a random name for you.
- Choose an organization (defaults to `personal`). Organizations are a way of sharing applications and resources between Fly.io users.
- Choose a region to deploy to. Defaults to the nearest Fly.io region. You can check out the [complete list of regions here](https://fly.io/docs/reference/regions/).
- Sets up a Postgres DB for you.
- Builds the Dockerfile.
- Deploys your application!
The `fly launch` command also created a `fly.toml` file for you. This is where you can set ENV values and other config.
### Storing secrets on Fly.io
You may also have some secrets you'd like to set on your app.
Use [`fly secrets`](https://fly.io/docs/reference/secrets/#setting-secrets) to configure those.
```console
$ fly secrets set MY_SECRET_KEY=my_secret_value
```
### Deploying again
When you want to deploy changes to your application, use `fly deploy`.
```console
$ fly deploy
```
Note: On Apple Silicon (M1) computers, docker runs cross-platform builds using qemu which might not always work. If you get a segmentation fault error like the following:
```console
=> [build 7/17] RUN mix deps.get --only
=> => # qemu: uncaught target signal 11 (Segmentation fault) - core dumped
```
You can use fly's remote builder by adding the `--remote-only` flag:
```console
$ fly deploy --remote-only
```
You can always check on the status of a deploy
```console
$ fly status
```
Check your app logs
```console
$ fly logs
```
If everything looks good, open your app on Fly
```console
$ fly open
```
## Clustering your application
Elixir and the Erlang VM have the incredible ability to be clustered together and pass messages seamlessly between nodes. Phoenix comes with all of the knobs in place, you only need to set the appropriate environment variables before deploying.
If you used `fly launch` to deploy your app, those environment variables are already in place, if not, open up `rel/env.ssh.eex` and add:
```sh
export ERL_AFLAGS="-proto_dist inet6_tcp"
export RELEASE_DISTRIBUTION="name"
export RELEASE_NODE="${FLY_APP_NAME}-${FLY_IMAGE_REF##*-}@${FLY_PRIVATE_IP}"
export ECTO_IPV6="true"
export DNS_CLUSTER_QUERY="${FLY_APP_NAME}.internal"
```
The first three environment variables are managed by Elixir and the Erlang/VM:
* `ERL_AFLAGS` - configures Erlang to use IPv6 for its distribution
* `RELEASE_DISTRIBUTION` - configures Erlang to named nodes
* `RELEASE_NODE` - attaches a name to the node, using Fly's app name and deploy reference
The last two are handled by your `config/runtime.exs`:
* `ECTO_IPV6` - connect to the database using IPv6
* `DNS_CLUSTER_QUERY` - configures your app to find other nodes using the given DNS query
## Extra Fly.io tips
### Getting an IEx shell into a running node
Elixir supports getting a IEx shell into a running production node.
There are a couple prerequisites, we first need to establish an [SSH Shell](https://fly.io/docs/flyctl/ssh/) to our machine on Fly.io.
This step sets up a root certificate for your account and then issues a certificate.
```console
$ fly ssh issue --agent
```
With SSH configured, let's open a console.
```console
$ fly ssh console
Connecting to my-app-1234.internal... complete
/ #
```
If all has gone smoothly, then you have a shell into the machine! Now we just need to launch our remote IEx shell. The deployment Dockerfile was configured to pull our application into `/app`. So the command for an app named `my_app` looks like this:
```console
$ app/bin/my_app remote
Erlang/OTP 23 [erts-11.2.1] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:1]
Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(my_app@fdaa:0:1da8:a7b:ac4:b204:7e29:2)1>
```
Now we have a running IEx shell into our node! You can safely disconnect using CTRL+C, CTRL+C.
#### Running multiple instances
There are two ways to run multiple instances.
1. Scale our application to have multiple instances in one region.
2. Add an instance to another region (multiple regions).
Let's first start with a baseline of our single deployment.
```console
$ fly status
...
Instances
ID VERSION REGION DESIRED STATUS HEALTH CHECKS RESTARTS CREATED
f9014bf7 26 sea run running 1 total, 1 passing 0 1h8m ago
```
#### Scaling in a single region
Let's scale up to 2 instances in our current region.
```console
$ fly scale count 2
Count changed to 2
```
Checking the status, we can see what happened.
```console
$ fly status
...
Instances
ID VERSION REGION DESIRED STATUS HEALTH CHECKS RESTARTS CREATED
eb4119d3 27 sea run running 1 total, 1 passing 0 39s ago
f9014bf7 27 sea run running 1 total, 1 passing 0 1h13m ago
```
We now have two instances in the same region.
Let's make sure they are clustered together. From an IEx shell, we can ask the node we're connected to, what other nodes it can see.
```console
$ fly ssh console -C "/app/bin/my_app remote"
```
```elixir
iex(my-app-1234@fdaa:0:1da8:a7b:ac2:f901:4bf7:2)1> Node.list
[:"my-app-1234@fdaa:0:1da8:a7b:ac4:eb41:19d3:2"]
```
The IEx prompt is included to help show the IP address of the node we are connected to. Then getting the `Node.list` returns the other node. Our two instances are connected and clustered!
#### Scaling to multiple regions
Fly makes it easy to deploy instances closer to your users. Through the magic of DNS, users are directed to the nearest region where your application is located. You can read more about [Fly.io regions here](https://fly.io/docs/reference/regions/).
Starting back from our baseline of a single instance running in `sea` which is Seattle, Washington (US), let's add the region `ewr` which is Parsippany, NJ (US). This puts an instance on both coasts of the US.
```console
$ fly regions add ewr
Region Pool:
ewr
sea
Backup Region:
iad
lax
sjc
vin
```
Looking at the status shows that we're only in 1 region because our count is set to 1.
```console
$ fly status
...
Instances
ID VERSION REGION DESIRED STATUS HEALTH CHECKS RESTARTS CREATED
cdf6c422 29 sea run running 1 total, 1 passing 0 58s ago
```
Let's add a 2nd instance and see it deploy to `ewr`.
```console
$ fly scale count 2
Count changed to 2
```
Now the status shows we have two instances spread across 2 regions!
```console
$ fly status
...
Instances
ID VERSION REGION DESIRED STATUS HEALTH CHECKS RESTARTS CREATED
0a8e6666 30 ewr run running 1 total, 1 passing 0 16s ago
cdf6c422 30 sea run running 1 total, 1 passing 0 6m47s ago
```
Let's ensure they are clustered together.
```console
$ fly ssh console -C "/app/bin/my_app remote"
```
```elixir
iex(my-app-1234@fdaa:0:1da8:a7b:ac2:cdf6:c422:2)1> Node.list
[:"my-app-1234@fdaa:0:1da8:a7b:ab2:a8e:6666:2"]
```
We have two instances of our application deployed to the West and East coasts of the North American continent and they are clustered together! Our users will automatically be directed to the server nearest them.
The Fly.io platform has built-in distribution support making it easy to cluster distributed Elixir nodes in multiple regions.
## Helpful Fly.io resources
Open the Dashboard for your account
```console
$ fly dashboard
```
Deploy your application
```console
$ fly deploy
```
Show the status of your deployed application
```console
$ fly status
```
Access and tail the logs
```console
$ fly logs
```
Scaling your application up or down
```console
$ fly scale count 2
```
Refer to the [Fly.io Elixir documentation](https://fly.io/docs/getting-started/elixir) for additional information.
[The Fly.io docs](https://fly.io/docs/) covers things like:
* [Status](https://fly.io/docs/flyctl/status/) and [logs](https://fly.io/docs/monitoring/logging-overview/)
* [Custom domains](https://fly.io/docs/networking/custom-domain/)
* [Certificates](https://fly.io/docs/networking/custom-domain-api/)
## Troubleshooting
See [Troubleshooting](https://fly.io/docs/getting-started/troubleshooting/) and [Elixir Troubleshooting](https://fly.io/docs/elixir/the-basics/troubleshooting/)
Visit the [Fly.io Community](https://community.fly.io/) to find solutions and ask questions.
================================================
FILE: guides/deployment/gigalixir.md
================================================
# Deploying on Gigalixir
Our main goal for this guide is to get a Phoenix application running on Gigalixir.
## What we'll need
The only thing we'll need for this guide is a working Phoenix application. For those of us who need a simple application to deploy, please follow the [Up and Running guide](https://hexdocs.pm/phoenix/up_and_running.html).
## Steps
Let's separate this process into a few steps, so we can keep track of where we are.
- Initialize Git repository
- Install the Gigalixir CLI
- Sign up for Gigalixir
- Create and set up Gigalixir application
- Provision a database
- Make our project ready for Gigalixir
- Deploy time!
- Useful Gigalixir commands
## Initializing Git repository
If you haven't already, we'll need to commit our files to git. We can do so by running the following commands in our project directory:
```console
$ git init
$ git add .
$ git commit -m "Initial commit"
```
## Installing the Gigalixir CLI
Follow the instructions [here](https://gigalixir.com/docs/getting-started-guide/) to install the command-line interface for your platform.
## Signing up for Gigalixir
We can sign up for an account at [gigalixir.com](https://gigalixir.com) or with the CLI. Let's use the CLI.
```console
$ gigalixir signup
# or with a Google account
$ gigalixir signup:google
```
Gigalixir’s free tier does not require a credit card and comes with 1 app instance and 1 PostgreSQL database for free, but please consider upgrading to a paid plan if you are running a production application.
Next, let's login
```console
$ gigalixir login
# or with a Google account
$ gigalixir login:google
```
And verify
```console
$ gigalixir account
```
## Creating and setting up our Gigalixir application
There are two different ways to deploy a Phoenix app on Gigalixir: with mix or with Elixir's releases. In this guide, we'll be using Elixir's releases because it is the recommended way. For more information, see [Elixir Releases vs Mix](https://gigalixir.com/docs/modify-app/#elixir-releases-vs-mix). If you want to deploy with the mix method, follow the [Phoenix deploy with Mix Guide](https://gigalixir.com/docs/getting-started-guide/phoenix-mix-deploy).
### Creating a Gigalixir application
Let's create a Gigalixir application
```console
$ gigalixir create -n "your-app-name"
```
Note: the app name cannot be changed afterwards. A random name is used if you do not provide one.
### Specifying versions
Gigalixir requires that you specify the Erlang and Elixir versions you intend to use. It's generally a good idea to run the same version in production as you do in development. For example:
```console
$ echo 'elixir_version=1.17.2' > elixir_buildpack.config
$ echo 'erlang_version=27.0' >> elixir_buildpack.config
$ git add elixir_buildpack.config
```
Gigalixir will use the latest nodejs version if you do not specify a version. If you want to specify your nodejs version, you can do so like this:
```console
$ echo 'node_version=22.7.0' > phoenix_static_buildpack.config
$ git add elixir_buildpack.config phoenix_static_buildpack.config assets/package.json
```
Finally, don't forget to commit:
```console
$ git commit -m "Set versions"
```
## Provisioning a database
Let's provision a database for our app. For a free database, run the following command
```console
$ gigalixir pg:create --free
```
For a production ready database, be sure to upgrade your account to the Standard Tier and create a Standard tier database
```console
$ gigalixir account:upgrade
$ gigalixir pg:create
```
Verify the database was created
```console
$ gigalixir pg
```
Verify that a `DATABASE_URL` and `POOL_SIZE` were created
```console
$ gigalixir config
```
## Making our Project ready for Gigalixir
There's nothing we need to do to get our app running on Gigalixir, but for a production app, you probably want to enforce SSL.
### Database Connection Security
You may also want to use SSL for your database connection. In your `config/runtime.exs`:
```elixir
ssl: [
verify: :verify_peer,
cacerts: :public_key.cacerts_get()
]
```
## Deploy Time!
Our project is now ready to be deployed on Gigalixir.
Be sure you have everything committed to git and run the following command:
```console
$ git push gigalixir
```
Check the status of your deploy and wait until the app is `Healthy`
```console
$ gigalixir ps
```
Run migrations
```console
$ gigalixir ps:migrate
```
Check your app logs
```console
$ gigalixir logs
```
If everything looks good, let's take a look at your app running on Gigalixir
```console
$ gigalixir open
```
## Useful Gigalixir Commands
Open a remote console
```console
$ gigalixir account:ssh_keys:add "$(cat ~/.ssh/id_rsa.pub)"
$ gigalixir ps:remote_console
```
To set up clustering, see [Clustering Nodes](https://gigalixir.com/docs/cluster)
For custom domains, scaling, jobs and other features, see the [Gigalixir Documentation](https://gigalixir.com/docs/).
## Troubleshooting
See [Troubleshooting](https://gigalixir.com/docs/troubleshooting) and the [FAQ](https://gigalixir.com/docs/faq)
Also, don't hesitate to email [help@gigalixir.com](mailto:help@gigalixir.com) or [request an invitation](https://elixir-lang.slack.com/join/shared_invite/zt-1f13hz7mb-N4KGjF523ONLCcHfb8jYgA#/shared-invite/email) and join the #gigalixir channel on [Slack](https://elixir-lang.slack.com).
================================================
FILE: guides/deployment/heroku.md
================================================
# Deploying on Heroku
Our main goal for this guide is to get a Phoenix application running on Heroku.
## What we'll need
The only thing we'll need for this guide is a working Phoenix application. For those of us who need a simple application to deploy, please follow the [Up and Running guide](https://hexdocs.pm/phoenix/up_and_running.html).
## Limitations
Heroku is a great platform and Elixir performs well on it. However, you may run into limitations if you plan to leverage advanced features provided by Elixir and Phoenix, such as:
- Connections are limited.
- Heroku [limits the number of simultaneous connections](https://devcenter.heroku.com/articles/http-routing#request-concurrency) as well as the [duration of each connection](https://devcenter.heroku.com/articles/limits#http-timeouts). It is common to use Elixir for real-time apps which need lots of concurrent, persistent connections, and Phoenix is capable of [handling over 2 million connections on a single server](https://www.phoenixframework.org/blog/the-road-to-2-million-websocket-connections).
- Distributed clustering is not possible.
- Heroku [firewalls dynos off from one another](https://devcenter.heroku.com/articles/dynos#networking). This means things like [distributed Phoenix channels](https://dockyard.com/blog/2016/01/28/running-elixir-and-phoenix-projects-on-a-cluster-of-nodes) and [distributed tasks](https://hexdocs.pm/elixir/distributed-tasks.html) will need to rely on something like Redis instead of Elixir's built-in distribution.
- In-memory state such as those in [Agents](https://hexdocs.pm/elixir/agents.html), [GenServers](https://hexdocs.pm/elixir/genservers.html), and [ETS](https://hexdocs.pm/elixir/erlang-term-storage.html) will be lost every 24 hours.
- Heroku [restarts dynos](https://devcenter.heroku.com/articles/dynos#restarting) every 24 hours regardless of whether the node is healthy.
- [The built-in observer](https://hexdocs.pm/elixir/debugging.html#observer) can't be used with Heroku.
- Heroku does allow for connection into your dyno, but you won't be able to use the observer to watch the state of your dyno.
If you are just getting started, or you don't expect to use the features above, Heroku should be enough for your needs. For instance, if you are migrating an existing application running on Heroku to Phoenix, keeping a similar set of features, Elixir will perform just as well or even better than your current stack.
If you want a platform-as-a-service without these limitations, there are alternatives listed in the sidebar and also generally available elsewhere. If you would rather deploy to a cloud platform, such as EC2, Google Cloud, etc, consider using `mix release`.
## Steps
Let's separate this process into a few steps, so we can keep track of where we are.
- Initialize Git repository
- Sign up for Heroku
- Install the Heroku Toolbelt
- Create and set up Heroku application
- Make our project ready for Heroku
- Deploy time!
- Useful Heroku commands
## Initializing Git repository
[Git](https://git-scm.com/) is a popular decentralized revision control system and is also used to deploy apps to Heroku.
Before we can push to Heroku, we'll need to initialize a local Git repository and commit our files to it. We can do so by running the following commands in our project directory:
```console
$ git init
$ git add .
$ git commit -m "Initial commit"
```
Heroku offers some great information on how it is using Git [here](https://devcenter.heroku.com/articles/git#prerequisites-install-git-and-the-heroku-cli).
## Signing up for Heroku
Signing up to Heroku is very simple, just head over to [https://signup.heroku.com/](https://signup.heroku.com/) and fill in the form.
The Free plan will give us one web [dyno](https://devcenter.heroku.com/articles/dynos) and one worker dyno, as well as a PostgreSQL and Redis instance for free.
These are meant to be used for testing and development, and come with some limitations. In order to run a production application, please consider upgrading to a paid plan.
## Installing the Heroku Toolbelt
Once we have signed up, we can download the correct version of the Heroku Toolbelt for our system [here](https://toolbelt.heroku.com/).
The Heroku CLI, part of the Toolbelt, is useful to create Heroku applications, list currently running dynos for an existing application, tail logs or run one-off commands (mix tasks for instance).
## Create and Set Up Heroku Application
There are two different ways to deploy a Phoenix app on Heroku. We could use Heroku buildpacks or their container stack. The difference between these two approaches is in how we tell Heroku to treat our build. In buildpack case, we need to update our apps configuration on Heroku to use Phoenix/Elixir specific buildpacks. On container approach, we have more control on how we want to set up our app, and we can define our container image using `Dockerfile` and `heroku.yml`. This section will explore the buildpack approach. In order to use Dockerfile, it is often recommended to convert our app to use releases, which we will describe later on.
### Create Application
A [buildpack](https://devcenter.heroku.com/articles/buildpacks) is a convenient way of packaging framework and/or runtime support. Phoenix requires 2 buildpacks to run on Heroku, the first adds basic Elixir support and the second adds Phoenix specific commands.
With the Toolbelt installed, let's create the Heroku application. We will do so using the latest available version of the [Elixir buildpack](https://github.com/HashNuke/heroku-buildpack-elixir):
```console
$ heroku create --buildpack hashnuke/elixir
Creating app... done, ⬢ mysterious-meadow-6277
Setting buildpack to hashnuke/elixir... done
https://mysterious-meadow-6277.herokuapp.com/ | https://git.heroku.com/mysterious-meadow-6277.git
```
> Note: the first time we use a Heroku command, it may prompt us to log in. If this happens, just enter the email and password you specified during signup.
> Note: the name of the Heroku application is the random string after "Creating" in the output above (mysterious-meadow-6277). This will be unique, so expect to see a different name from "mysterious-meadow-6277".
> Note: the URL in the output is the URL to our application. If we open it in our browser now, we will get the default Heroku welcome page.
> Note: if we hadn't initialized our Git repository before we ran the `heroku create` command, we wouldn't have our Heroku remote repository properly set up at this point. We can set that up manually by running: `heroku git:remote -a [our-app-name].`
The buildpack uses a predefined Elixir and Erlang version, but to avoid surprises when deploying, it is best to explicitly list the Elixir and Erlang version we want in production to be the same we are using during development or in your continuous integration servers. This is done by creating a config file named `elixir_buildpack.config` in the root directory of your project with your target version of Elixir and Erlang:
```console
# Elixir version
elixir_version=1.15.0
# Erlang version
# https://github.com/HashNuke/heroku-buildpack-elixir-otp-builds/blob/master/otp-versions
erlang_version=25.3
# Invoke assets.deploy defined in your mix.exs to deploy assets with esbuild
# Note we nuke the esbuild executable from the image
hook_post_compile="eval mix assets.deploy && rm -f _build/esbuild*"
```
Finally, let's tell the build pack how to start our webserver. Create a file named `Procfile` at the root of your project:
```console
web: mix phx.server
```
### Optional: Node, npm, and the Phoenix Static buildpack
By default, Phoenix uses `esbuild` and manages all assets for you. However, if you are using `node` and `npm`, you will need to install the [Phoenix Static buildpack](https://github.com/gigalixir/gigalixir-buildpack-phoenix-static) to handle them:
```console
$ heroku buildpacks:add https://github.com/gigalixir/gigalixir-buildpack-phoenix-static.git
Buildpack added. Next release on mysterious-meadow-6277 will use:
1. https://github.com/HashNuke/heroku-buildpack-elixir.git
2. https://github.com/gigalixir/gigalixir-heroku-buildpack-phoenix-static.git
```
When using this buildpack, you want to delegate all asset bundling to `npm`. So you must remove the `hook_post_compile` configuration from your `elixir_buildpack.config` and move it to the deploy script of your `assets/package.json`. Something like this:
```javascript
{
...
"scripts": {
"deploy": "cd .. && mix assets.deploy && rm -f _build/esbuild*"
}
...
}
```
The Phoenix Static buildpack uses a predefined Node.js version, but to avoid surprises when deploying, it is best to explicitly list the Node.js version we want in production to be the same we are using during development or in your continuous integration servers. This is done by creating a config file named `phoenix_static_buildpack.config` in the root directory of your project with your target version of Node.js:
```text
# Node.js version
node_version=10.20.1
```
Please refer to the [configuration section](https://github.com/gigalixir/gigalixir-buildpack-phoenix-static#configuration) for full details. You can make your own custom build script, but for now we will use the [default one provided](https://github.com/gigalixir/gigalixir-buildpack-phoenix-static/blob/master/compile).
Finally, note that since we are using multiple buildpacks, you might run into an issue where the sequence is out of order (the Elixir buildpack needs to run before the Phoenix Static buildpack). [Heroku's docs](https://devcenter.heroku.com/articles/using-multiple-buildpacks-for-an-app) explain this better, but you will need to make sure the Phoenix Static buildpack comes last.
## Making our Project ready for Heroku
Every new Phoenix project ships with a config file `config/runtime.exs` which loads configuration and secrets from [environment variables](https://devcenter.heroku.com/articles/config-vars). This aligns well with Heroku best practices ([12-factor apps](https://12factor.net/)), so the only work left for us to do is to configure URLs and SSL.
First let's tell Phoenix to only use the SSL version of the website. Find the endpoint config in your `config/prod.exs`:
```elixir
config :scaffold, ScaffoldWeb.Endpoint,
url: [port: 443, scheme: "https"],
```
... and add `force_ssl`
```elixir
config :scaffold, ScaffoldWeb.Endpoint,
url: [port: 443, scheme: "https"],
force_ssl: [rewrite_on: [:x_forwarded_proto]],
```
`force_ssl` need to be set here because it is a _compile_ time config. It will not work when set from `runtime.exs`.
Then in your `config/runtime.exs`:
... add `host`
```elixir
config :scaffold, ScaffoldWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"]
```
and uncomment the `# ssl: true,` line in your repository configuration. It will look like this:
```elixir
config :hello, Hello.Repo,
ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
```
Finally, if you plan on using websockets, then we will need to decrease the timeout for the websocket transport in `lib/hello_web/endpoint.ex`. If you do not plan on using websockets, then leaving it set to false is fine. You can find further explanation of the options available at the [documentation](https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration).
```elixir
defmodule HelloWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :hello
socket "/socket", HelloWeb.UserSocket,
websocket: [timeout: 45_000]
...
end
```
Also set the host in Heroku:
```console
$ heroku config:set PHX_HOST="mysterious-meadow-6277.herokuapp.com"
```
This ensures that any idle connections are closed by Phoenix before they reach Heroku's 55-second timeout window.
## Creating Environment Variables in Heroku
The `DATABASE_URL` config var is automatically created by Heroku when we add the [Heroku Postgres add-on](https://elements.heroku.com/addons/heroku-postgresql). We can create the database via the Heroku toolbelt:
```console
$ heroku addons:create heroku-postgresql:mini
```
Now we set the `POOL_SIZE` config var:
```console
$ heroku config:set POOL_SIZE=18
```
This value should be just under the number of available connections, leaving a couple open for migrations and mix tasks. The mini database allows 20 connections, so we set this number to 18. If additional dynos will share the database, reduce the `POOL_SIZE` to give each dyno an equal share.
When running a mix task later (after we have pushed the project to Heroku) you will also want to limit its pool size like so:
```console
$ heroku run "POOL_SIZE=2 mix hello.task"
```
So that Ecto does not attempt to open more than the available connections.
We still have to create the `SECRET_KEY_BASE` config based on a random string. First, use `mix phx.gen.secret` to get a new secret:
```console
$ mix phx.gen.secret
xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53
```
Your random string will be different; don't use this example value.
Now set it in Heroku:
```console
$ heroku config:set SECRET_KEY_BASE="xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53"
Setting config vars and restarting mysterious-meadow-6277... done, v3
SECRET_KEY_BASE: xvafzY4y01jYuzLm3ecJqo008dVnU3CN4f+MamNd1Zue4pXvfvUjbiXT8akaIF53
```
## Deploy Time!
Our project is now ready to be deployed on Heroku.
Let's commit all our changes:
```console
$ git add elixir_buildpack.config
$ git commit -a -m "Use production config from Heroku ENV variables and decrease socket timeout"
```
And deploy:
```console
$ git push heroku main
Counting objects: 55, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (49/49), done.
Writing objects: 100% (55/55), 48.48 KiB | 0 bytes/s, done.
Total 55 (delta 1), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Multipack app detected
remote: -----> Fetching custom git buildpack... done
remote: -----> elixir app detected
remote: -----> Checking Erlang and Elixir versions
remote: WARNING: elixir_buildpack.config wasn't found in the app
remote: Using default config from Elixir buildpack
remote: Will use the following versions:
remote: * Stack cedar-14
remote: * Erlang 17.5
remote: * Elixir 1.0.4
remote: Will export the following config vars:
remote: * Config vars DATABASE_URL
remote: * MIX_ENV=prod
remote: -----> Stack changed, will rebuild
remote: -----> Fetching Erlang 17.5
remote: -----> Installing Erlang 17.5 (changed)
remote:
remote: -----> Fetching Elixir v1.0.4
remote: -----> Installing Elixir v1.0.4 (changed)
remote: -----> Installing Hex
remote: 2015-07-07 00:04:00 URL:https://s3.amazonaws.com/s3.hex.pm/installs/1.0.0/hex.ez [262010/262010] ->
"/app/.mix/archives/hex.ez" [1]
remote: * creating /app/.mix/archives/hex.ez
remote: -----> Installing rebar
remote: * creating /app/.mix/rebar
remote: -----> Fetching app dependencies with mix
remote: Running dependency resolution
remote: Dependency resolution completed successfully
remote: [...]
remote: -----> Compiling
remote: [...]
remote: Generated phoenix_heroku app
remote: [...]
remote: Consolidated protocols written to _build/prod/consolidated
remote: -----> Creating .profile.d with env vars
remote: -----> Fetching custom git buildpack... done
remote: -----> Phoenix app detected
remote:
remote: -----> Loading configuration and environment
remote: Loading config...
remote: [...]
remote: Will export the following config vars:
remote: * Config vars DATABASE_URL
remote: * MIX_ENV=prod
remote:
remote: -----> Compressing... done, 82.1MB
remote: -----> Launching... done, v5
remote: https://mysterious-meadow-6277.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/mysterious-meadow-6277.git
* [new branch] master -> master
```
Typing `heroku open` in the terminal should launch a browser with the Phoenix welcome page opened. In the event that you are using Ecto to access a database, you will also need to run migrations after the first deploy:
```console
$ heroku run "POOL_SIZE=2 mix ecto.migrate"
```
And that's it!
## Deploying to Heroku using the container stack
### Create Heroku application
Set the stack of your app to `container`, this allows us to use `Dockerfile` to define our app setup.
```console
$ heroku create
Creating app... done, ⬢ mysterious-meadow-6277
$ heroku stack:set container
```
Add a new `heroku.yml` file to your root folder. In this file you can define addons used by your app, how to build the image and what configs are passed to the image. You can learn more about Heroku's `heroku.yml` options [here](https://devcenter.heroku.com/articles/build-docker-images-heroku-yml). Here is a sample:
```yaml
setup:
addons:
- plan: heroku-postgresql
as: DATABASE
build:
docker:
web: Dockerfile
config:
MIX_ENV: prod
SECRET_KEY_BASE: $SECRET_KEY_BASE
DATABASE_URL: $DATABASE_URL
```
### Set up releases and Dockerfile
Now we need to define a `Dockerfile` at the root folder of your project that contains your application. We recommend to use releases when doing so, as the release will allow us to build a container with only the parts of Erlang and Elixir we actually use. Follow the [releases docs](releases.html). At the end of the guide, there is a sample Dockerfile file you can use.
Once you have the image definition set up, you can push your app to heroku and you can see it starts building the image and deploy it.
## Useful Heroku Commands
We can look at the logs of our application by running the following command in our project directory:
```console
$ heroku logs # use --tail if you want to tail them
```
We can also start an IEx session attached to our terminal for experimenting in our app's environment:
```console
$ heroku run "POOL_SIZE=2 iex -S mix"
```
In fact, we can run anything using the `heroku run` command, like the Ecto migration task from above:
```console
$ heroku run "POOL_SIZE=2 mix ecto.migrate"
```
## Connecting to your dyno
Heroku gives you the ability to connect to your dyno with an IEx shell which allows running Elixir code such as database queries.
- Modify the `web` process in your Procfile to run a named node:
```text
web: elixir --sname server -S mix phx.server
```
- Redeploy to Heroku
- Connect to the dyno with `heroku ps:exec` (if you have several applications on the same repository you will need to specify the app name or the remote name with `--app APP_NAME` or `--remote REMOTE_NAME`)
- Launch an iex session with `iex --sname console --remsh server`
You have an iex session into your dyno!
## Troubleshooting
### Compilation Error
Occasionally, an application will compile locally, but not on Heroku. The compilation error on Heroku will look something like this:
```console
remote: == Compilation error on file lib/postgrex/connection.ex ==
remote: could not compile dependency :postgrex, "mix compile" failed. You can recompile this dependency with "mix deps.compile postgrex", update it with "mix deps.update postgrex" or clean it with "mix deps.clean postgrex"
remote: ** (CompileError) lib/postgrex/connection.ex:207: Postgrex.Connection.__struct__/0 is undefined, cannot expand struct Postgrex.Connection
remote: (elixir) src/elixir_map.erl:58: :elixir_map.translate_struct/4
remote: (stdlib) lists.erl:1353: :lists.mapfoldl/3
remote: (stdlib) lists.erl:1354: :lists.mapfoldl/3
remote:
remote:
remote: ! Push rejected, failed to compile elixir app
remote:
remote: Verifying deploy...
remote:
remote: ! Push rejected to mysterious-meadow-6277.
remote:
To https://git.heroku.com/mysterious-meadow-6277.git
```
This has to do with stale dependencies which are not getting recompiled properly. It's possible to force Heroku to recompile all dependencies on each deploy, which should fix this problem. The way to do it is to add a new file called `elixir_buildpack.config` at the root of the application. The file should contain this line:
```text
always_rebuild=true
```
Commit this file to the repository and try to push again to Heroku.
### Connection Timeout Error
If you are constantly getting connection timeouts while running `heroku run` this could mean that your internet provider has blocked port number 5000:
```console
heroku run "POOL_SIZE=2 mix myapp.task"
Running POOL_SIZE=2 mix myapp.task on mysterious-meadow-6277... !
ETIMEDOUT: connect ETIMEDOUT 50.19.103.36:5000
```
You can overcome this by adding `detached` option to run command:
```console
heroku run:detached "POOL_SIZE=2 mix ecto.migrate"
Running POOL_SIZE=2 mix ecto.migrate on mysterious-meadow-6277... done, run.8089 (Free)
```
================================================
FILE: guides/deployment/releases.md
================================================
# Deploying with Releases
Our main goal for this guide is to package your Phoenix application into a self-contained directory that includes the Erlang VM, Elixir, all of your code and dependencies. This package can then be dropped into a production machine.
## What we'll need
The only thing we'll need for this guide is a working Phoenix application. For those of us who need a simple application to deploy, please follow the [Up and Running guide](up_and_running.html).
## Releases, assemble!
If you are not familiar with Elixir releases yet, we recommend you to read [Elixir's excellent docs](https://hexdocs.pm/mix/Mix.Tasks.Release.html) before continuing.
Once that is done, you can assemble a release by going through all of the steps in our general [deployment guide](deployment.html) with `mix release` at the end. Let's recap.
First set the environment variables:
```console
$ mix phx.gen.secret
REALLY_LONG_SECRET
$ export SECRET_KEY_BASE=REALLY_LONG_SECRET
$ export DATABASE_URL=ecto://USER:PASS@HOST/database
```
Then load dependencies to compile code and assets:
```console
# Initial setup
$ mix deps.get --only prod
$ MIX_ENV=prod mix compile
# Compile assets
$ MIX_ENV=prod mix assets.deploy
```
And now run `mix phx.gen.release`:
```console
$ mix phx.gen.release
==> my_app
* creating rel/overlays/bin/server
* creating rel/overlays/bin/server.bat
* creating rel/overlays/bin/migrate
* creating rel/overlays/bin/migrate.bat
* creating lib/my_app/release.ex
Your application is ready to be deployed in a release!
# To start your system
_build/dev/rel/my_app/bin/my_app start
# To start your system with the Phoenix server running
_build/dev/rel/my_app/bin/server
# To run migrations
_build/dev/rel/my_app/bin/migrate
Once the release is running:
# To connect to it remotely
_build/dev/rel/my_app/bin/my_app remote
# To stop it gracefully (you may also send SIGINT/SIGTERM)
_build/dev/rel/my_app/bin/my_app stop
To list all commands:
_build/dev/rel/my_app/bin/my_app
```
The `phx.gen.release` task generated a few files for us to assist in releases. First, it created `server` and `migrate` *overlay* scripts for conveniently running the phoenix server inside a release or invoking migrations from a release. The files in the `rel/overlays` directory are copied into every release environment. Next, it generated a `release.ex` file which is used to invoke Ecto migrations without a dependency on `mix` itself.
*Note*: If you are a Docker user, you can pass the `--docker` flag to `mix phx.gen.release` to generate a Dockerfile ready for deployment.
Next, we can invoke `mix release` to build the release:
```console
$ MIX_ENV=prod mix release
Generated my_app app
* assembling my_app-0.1.0 on MIX_ENV=prod
* using config/runtime.exs to configure the release at runtime
Release created at _build/prod/rel/my_app!
# To start your system
_build/prod/rel/my_app/bin/my_app start
...
```
You can start the release by calling `_build/prod/rel/my_app/bin/my_app start`, or boot your webserver by calling `_build/prod/rel/my_app/bin/server`, where you have to replace `my_app` by your current application name.
Now you can get all of the files under the `_build/prod/rel/my_app` directory, package it, and run it in any production machine with the same OS and architecture as the one that assembled the release. For more details, check the [docs for `mix release`](https://hexdocs.pm/mix/Mix.Tasks.Release.html).
## Ecto migrations
A common need in production systems is to execute custom commands required to set up the production environment. One of such commands is precisely migrating the database. Since we don't have `Mix`, a *build* tool, inside releases, which are production artifacts, we need to bring said commands directly into the release.
The `phx.gen.release` command created the following `release.ex` file in your project `lib/my_app/release.ex`, with the following content:
```elixir
defmodule MyApp.Release do
@app :my_app
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
Application.ensure_all_started(:ssl)
Application.ensure_loaded(@app)
end
end
```
Where you replace the first two lines by your application names.
Now you can assemble a new release with `MIX_ENV=prod mix release` and you can invoke any code, including the functions in the module above, by calling the `eval` command:
```console
$ _build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate"
```
And that's it! If you peek inside the `migrate` script, you'll see it wraps exactly this invocation. Depending on where you are deploying your application, you can invoke the `migrate` command separately, or you may want to change the `server` script to migrate your database before starting your app.
## Custom commands
You can use the same approach used for migrations to create any custom command to run in production. The idea is that each command invokes `load_app`, which calls `Application.ensure_loaded/1` to load the current application without starting it.
However, some commands may need to start the whole application. In such cases, `Application.ensure_all_started/1` must be used instead of `Application.load/1`. Keep in mind starting the application will start all processes in its supervision tree, including the Phoenix endpoint. This can be circumvented by changing your supervision tree to not start certain children under certain conditions. For example, in the release commands file you could do:
```elixir
defp start_app do
load_app()
Application.put_env(@app, :minimal, true)
Application.ensure_all_started(@app)
end
```
And then in your application you check `Application.get_env(@app, :minimal)` and start only part of the children when it is set.
## Containers
Elixir releases work well with container technologies such as Docker. The idea is that you assemble the release inside the Docker container and then build an image based on the release artifacts.
If you call `mix phx.gen.release --docker`, you'll see a new file with content similar to:
```Dockerfile
# Find eligible builder and runner images on Docker Hub. We use Ubuntu/Debian
# instead of Alpine to avoid DNS resolution issues in production.
#
# https://hub.docker.com/r/hexpm/elixir/tags?page=1&name=ubuntu
# https://hub.docker.com/_/ubuntu?tab=tags
#
# This file is based on these images:
#
# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
# - https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye-20230612-slim - for the release image
# - https://pkgs.org/ - resource for finding needed packages
# - Ex: hexpm/elixir:1.18.4-erlang-27.3.4.3-debian-trixie-20250908-slim
#
ARG ELIXIR_VERSION=1.18.4
ARG OTP_VERSION=27.3.4.3
ARG DEBIAN_VERSION=trixie-20250908-slim
ARG BUILDER_IMAGE="hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}"
ARG RUNNER_IMAGE="debian:${DEBIAN_VERSION}"
FROM ${BUILDER_IMAGE} AS builder
# install build dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential git \
&& rm -rf /var/lib/apt/lists/*
# prepare build dir
WORKDIR /app
# install hex + rebar
RUN mix local.hex --force \
&& mix local.rebar --force
# set build ENV
ENV MIX_ENV="prod"
# install mix dependencies
COPY mix.exs mix.lock ./
RUN mix deps.get --only $MIX_ENV
RUN mkdir config
# copy compile-time config files before we compile dependencies
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/
RUN mix deps.compile
RUN mix assets.setup
COPY priv priv
COPY lib lib
# Compile the release
RUN mix compile
COPY assets assets
# compile assets
RUN mix assets.deploy
# Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/
COPY rel rel
RUN mix release
# start a new build stage so that the final image will only contain
# the compiled release and other runtime necessities
FROM ${RUNNER_IMAGE} AS final
RUN apt-get update \
&& apt-get install -y --no-install-recommends libstdc++6 openssl libncurses6 locales ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Set the locale
RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \
&& locale-gen
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8
WORKDIR "/app"
RUN chown nobody /app
# set runner ENV
ENV MIX_ENV="prod"
# Only copy the final release from the build stage
COPY --from=builder --chown=nobody:root /app/_build/${MIX_ENV}/rel/my_app ./
USER nobody
# If using an environment that doesn't automatically reap zombie processes, it is
# advised to add an init process such as tini via `apt-get install`
# above and adding an entrypoint. See https://github.com/krallin/tini for details
# ENTRYPOINT ["/tini", "--"]
CMD ["/app/bin/server"]
```
Where `my_app` is the name of your app. At the end, you will have an application in `/app` ready to run as `/app/bin/server`.
A few points about configuring a containerized application:
- The more configuration you can provide at runtime (using `config/runtime.exs`), the more reusable your images will be across environments. In particular, secrets like database credentials and API keys should not be compiled into the image, but rather should be provided when creating containers based on that image. This is why the `Endpoint`'s `:secret_key_base` is configured in `config/runtime.exs` by default.
- If possible, any environment variables that are needed at runtime should be read in `config/runtime.exs`, not scattered throughout your code. Having them all visible in one place will make it easier to ensure the containers get what they need, especially if the person doing the infrastructure work does not work on the Elixir code. Libraries in particular should never directly read environment variables; all their configuration should be handed to them by the top-level application, preferably [without using the application environment](https://hexdocs.pm/elixir/design-anti-patterns.html#using-application-configuration-for-libraries).
## Clustering
Elixir and the Erlang VM have the incredible ability to be clustered together and pass messages seamlessly between nodes. To enable clustering, we need two distinct features:
* Node connection: different instances of the same service should communicate with each other. This is a feature of the Erlang VM.
* Service discovery: for a given service, you must be able to find the IP address of all instances. Phoenix ships with `dns_cluster` to provide out-of-the-box DNS-based service discovery but alternative methods may be used.
### DNS Discovery
Your clustering configuration is typically added to `rel/env.sh.eex`. This is a file that is executed before you release starts, and it is a perfect place to configure your application runtime based on your deployment environment. Here is a general skeleton:
```sh
# Uncomment if IPv6 is required
# export ECTO_IPV6="true"
# export ERL_AFLAGS="-proto_dist inet6_tcp"
# Erlang uses a port mapper daemon on each node,
# it by default runs on port 4369
export ERL_EPMD_PORT=4369
# Use the ports 4370-4372 for nodes to communicate.
export ERL_AFLAGS="-kernel inet_dist_listen_min 4370 inet_dist_listen_max 4372"
export RELEASE_DISTRIBUTION="name"
export RELEASE_NODE="app-${PLATFORM_DEPLOYMENT_SHA}@${PLATFORM_DEPLOYMENT_IP}"
export DNS_CLUSTER_QUERY="your-app.internal"
```
The script above is doing a couple things:
* It configures your app to use ports 4369, 4370, 4371, and 4372 for communication. You may need to explicitly expose those as internal TCP ports in your deployment platform (in addition to the HTTP port of your choice)
* It then configures your app to use fully qualified names. The name of each app will include the current deployment sha as `PLATFORM_DEPLOYMENT_SHA` (the name of the exact environment variable is platform dependent), so each deployment establishes its own cluster, and the current IP as `PLATFORM_DEPLOYMENT_IP` (also platform specific). If the IP is not available, you may be able to compute it as `NODE_IP=hostname | tr -d ' '`
* Then finally you define a DNS query which will be used to find the IPs of the other instances
Some platforms, such as [Fly.io](https://fly.io/docs/networking/private-networking/), [Railway](https://docs.railway.com/guides/private-networking), and [Render](https://render.com/docs/private-network#direct-ip-communication-advanced), provide private networks with DNS querying out of the box. You only need to adapt the `DNS_CLUSTER_QUERY` variable accordingly.
Other platforms, such as [Digital Ocean App Platform](https://www.digitalocean.com/products/app-platform) and [Northflank](https://northflank.com/features/platform), allow nodes to directly connect to each other, but they do not provide DNS service discovery. In this next section, we explore different service discovery mechanisms.
### Alternative discovery mechanisms
While not all platforms support DNS queries for service discovery, there are many alternative strategies for connecting your nodes together. Please checkout the following libraries:
* [libcluster](https://github.com/bitwalker/libcluster) - provides strategies for connecting your nodes using gossip protocols, kubernetes, ec2, and others
* [libcluster_postgres](https://github.com/supabase/libcluster_postgres/) - a plugin for `libcluster` which uses PostgreSQL for node discovery. Given most applications already use a database, and likely PostgreSQL, this is a suitable option which does not require additional setup
When using the libraries above, you can likely remove `dns_query` from your application dependencies.
### `epmd`-less deployment
In the snippet above, we used ports 4369, 4370, 4371, and 4372. However, the Erlang VM allows running the distribution over a fixed port, also known as `epmd`-less deployments. To enable such, do this.
Remove the lines:
```sh
export ERL_EPMD_PORT=4369
export ERL_AFLAGS="-kernel inet_dist_listen_min 4370 inet_dist_listen_max 4372"
```
Add a file rel/vm.args.eex with the following:
```
-start_epmd false -erl_epmd_port 6789
```
Add a file rel/remote.vm.args.eex with the following:
```
-start_epmd false -erl_epmd_port 6789 -dist_listen false
```
And now only port 6789 (in addition to the HTTP one) needs to be exposed internally between instances.
================================================
FILE: guides/directory_structure.md
================================================
# Directory structure
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
When we use `mix phx.new` to generate a new Phoenix application, it builds a top-level directory structure like this:
```console
├── _build
├── assets
├── config
├── deps
├── lib
│ ├── hello
│ ├── hello.ex
│ ├── hello_web
│ └── hello_web.ex
├── priv
└── test
```
We will go over those directories one by one:
* `_build` - a directory that holds all compilation artifacts, created by the `mix` command line tool (shipped as part of Elixir). As we have seen in "[Up and Running](up_and_running.html)", `mix` is the main interface to your application. We use Mix to compile our code, create databases, run our server, and more. This directory must not be checked into version control and it can be removed at any time. Removing it will force Mix to rebuild your application from scratch.
* `assets` - a directory that keeps source code for your front-end assets, typically JavaScript and CSS. These sources are automatically bundled by the `esbuild` tool. Static files like images and fonts go in `priv/static`.
* `config` - a directory that holds your project configuration. The `config/config.exs` file is the entry point for your configuration. At the end of the `config/config.exs`, it imports environment specific configuration, which can be found in `config/dev.exs`, `config/test.exs`, and `config/prod.exs`. Finally, `config/runtime.exs` is executed and it is the best place to read secrets and other dynamic configuration.
* `deps` - a directory with all of our Mix dependencies. You can find all dependencies listed in the `mix.exs` file, inside the `defp deps do` function definition. This directory must not be checked into version control and it can be removed at any time. Removing it will force Mix to download all deps from scratch.
* `lib` - a directory that holds your application source code. This directory is broken into two subdirectories, `lib/hello` and `lib/hello_web`. The `lib/hello` directory is responsible for hosting all of your business logic and business domain. It typically interacts directly with the database - it is the "Model" in Model-View-Controller (MVC) architecture. `lib/hello_web` is responsible for exposing your business domain to the world, in this case, through a web application. It holds both the View and Controller from MVC. We will discuss the contents of these directories in more detail in the next sections.
* `priv` - a directory that keeps all resources that are necessary in production but are not directly part of your source code. You typically keep database scripts, translation files, images, and more in here. Generated assets, created from files in the `assets` directory, are placed in `priv/static/assets` by default.
* `test` - a directory with all of our application tests. It often mirrors the same structure found in `lib`.
## The lib/hello directory
The `lib/hello` directory hosts all of your business domain. Since our project does not have any business logic yet, the directory is mostly empty. You will only find three files:
```console
lib/hello
├── application.ex
├── mailer.ex
└── repo.ex
```
The `lib/hello/application.ex` file defines an Elixir application named `Hello.Application`. That's because at the end of the day Phoenix applications are simply Elixir applications. The `Hello.Application` module defines which services are part of our application:
```elixir
children = [
HelloWeb.Telemetry,
Hello.Repo,
{Phoenix.PubSub, name: Hello.PubSub},
HelloWeb.Endpoint
]
```
If it is your first time with Phoenix, you don't need to worry about the details right now. For now, suffice it to say our application starts a database repository, a PubSub system for sharing messages across processes and nodes, and the application endpoint, which effectively serves HTTP requests. These services are started in the order they are defined and, whenever shutting down your application, they are stopped in the reverse order.
You can learn more about applications in [Elixir's official docs for Application](https://hexdocs.pm/elixir/Application.html).
The `lib/hello/mailer.ex` file holds the `Hello.Mailer` module, which defines the main interface to deliver e-mails:
```elixir
defmodule Hello.Mailer do
use Swoosh.Mailer, otp_app: :hello
end
```
In the same `lib/hello` directory, we will find a `lib/hello/repo.ex`. It defines a `Hello.Repo` module which is our main interface to the database. If you are using Postgres (the default database), you will see something like this:
```elixir
defmodule Hello.Repo do
use Ecto.Repo,
otp_app: :hello,
adapter: Ecto.Adapters.Postgres
end
```
And that's it for now. As you work on your project, we will add files and modules to this directory.
## The lib/hello_web directory
The `lib/hello_web` directory holds the web-related parts of our application. It looks like this when expanded:
```console
lib/hello_web
├── controllers
│ ├── page_controller.ex
│ ├── page_html.ex
│ ├── error_html.ex
│ ├── error_json.ex
│ └── page_html
│ └── home.html.heex
├── components
│ ├── core_components.ex
│ ├── layouts.ex
│ └── layouts
│ └── root.html.heex
├── endpoint.ex
├── gettext.ex
├── router.ex
└── telemetry.ex
```
All of the files which are currently in the `controllers` and `components` directories are there to create the "Welcome to Phoenix!" page we saw in the "[Up and running](up_and_running.html)" guide.
By looking at `controller` and `components` directories, we can see Phoenix provides features for handling layouts, HTML, and error pages out of the box.
Besides the directories mentioned, `lib/hello_web` has four files at its root. `lib/hello_web/endpoint.ex` is the entry-point for HTTP requests. Once the browser accesses [http://localhost:4000](http://localhost:4000), the endpoint starts processing the data, eventually leading to the router, which is defined in `lib/hello_web/router.ex`. The router defines the rules to dispatch requests to "controllers", which calls a view module to render HTML pages back to clients. We explore these layers in length in other guides, starting with the "[Request life-cycle](request_lifecycle.html)" guide coming next.
Through _Telemetry_, Phoenix is able to collect metrics and send monitoring events of your application. The `lib/hello_web/telemetry.ex` file defines the supervisor responsible for managing the telemetry processes. You can find more information on this topic in the [Telemetry guide](telemetry.html).
Finally, there is a `lib/hello_web/gettext.ex` file which provides internationalization through [Gettext](https://hexdocs.pm/gettext/Gettext.html). If you are not worried about internationalization, you can safely skip this file and its contents.
## The assets directory
The `assets` directory contains source files related to front-end assets, such as JavaScript and CSS. Since Phoenix v1.6, we use [`esbuild`](https://github.com/evanw/esbuild/) to compile assets, which is managed by the [`esbuild`](https://github.com/phoenixframework/esbuild) Elixir package. The integration with `esbuild` is baked into your app. The relevant config can be found in your `config/config.exs` file.
Your other static assets are placed in the `priv/static` folder, where `priv/static/assets` is kept for generated assets. Everything in `priv/static` is served by the `Plug.Static` plug configured in `lib/hello_web/endpoint.ex`. When running in dev mode (`MIX_ENV=dev`), Phoenix watches for any changes you make in the `assets` directory, and then takes care of updating your front end application in your browser as you work.
Note that when you first create your Phoenix app using `mix phx.new` it is possible to specify options that will affect the presence and layout of the `assets` directory. In fact, Phoenix apps can bring their own front end tools or not have a front-end at all (handy if you're writing an API for example). For more information you can run `mix help phx.new`.
If the default esbuild integration does not cover your needs, for example because you want to use another build tool, you can switch to a [custom assets build](asset_management.html#custom_builds).
As for CSS, Phoenix ships with the [Tailwind CSS Framework](https://tailwindcss.com/), providing a base setup for projects. You may move to any CSS framework of your choice. Additional references can be found in the [asset management](asset_management.md#css) guide.
================================================
FILE: guides/ecto.md
================================================
# Ecto
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
Most web applications today need some form of data validation and persistence. In the Elixir ecosystem, we have `Ecto` to enable this. Before we jump into building database-backed web features, we're going to focus on the finer details of Ecto to give a solid base to build our web features on top of. Let's get started!
Phoenix uses Ecto to provide builtin support to the following databases:
* PostgreSQL (via [`postgrex`](https://github.com/elixir-ecto/postgrex))
* MySQL (via [`myxql`](https://github.com/elixir-ecto/myxql))
* MSSQL (via [`tds`](https://github.com/livehelpnow/tds))
* ETS (via [`etso`](https://github.com/evadne/etso))
* SQLite3 (via [`ecto_sqlite3`](https://github.com/elixir-sqlite/ecto_sqlite3))
Newly generated Phoenix projects include Ecto with the PostgreSQL adapter by default. You can pass the `--database` option to change or `--no-ecto` flag to exclude this.
Ecto also provides support for other databases and it has many learning resources available. Please check out [Ecto's README](https://github.com/elixir-ecto/ecto) for general information.
This guide assumes that we have generated our new application with Ecto integration and that we will be using PostgreSQL. The introductory guides cover how to get your first application up and running. For using other databases, see the [Swapping Databases](swapping_databases.html) how-to guide.
## Using `phx.gen.schema`
Once we have Ecto and PostgreSQL installed and configured, the easiest way to use Ecto is to generate an Ecto *schema* through the `phx.gen.schema` task. Ecto schemas are a way for us to specify how Elixir data types map to and from external sources, such as database tables. Let's generate a `User` schema with `name`, `email`, `bio`, and `number_of_pets` fields.
```console
$ mix phx.gen.schema User users name:string email:string \
bio:string number_of_pets:integer
* creating ./lib/hello/user.ex
* creating priv/repo/migrations/20170523151118_create_users.exs
Remember to update your repository by running migrations:
$ mix ecto.migrate
```
A couple of files were generated with this task. First, we have a `user.ex` file, containing our Ecto schema with our schema definition of the fields we passed to the task. Next, a migration file was generated inside `priv/repo/migrations/` which will create our database table that our schema maps to.
With our files in place, let's follow the instructions and run our migration:
```console
$ mix ecto.migrate
Compiling 1 file (.ex)
Generated hello app
[info] == Running Hello.Repo.Migrations.CreateUsers.change/0 forward
[info] create table users
[info] == Migrated in 0.0s
```
Mix assumes that we are in the development environment unless we tell it otherwise with `MIX_ENV=prod mix ecto.migrate`.
If we log in to our database server, and connect to our `hello_dev` database, we should see our `users` table. Ecto assumes that we want an integer column called `id` as our primary key, so we should see a sequence generated for that as well.
```console
$ psql -U postgres
Type "help" for help.
postgres=# \connect hello_dev
You are now connected to database "hello_dev" as user "postgres".
hello_dev=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------------+----------+----------
public | schema_migrations | table | postgres
public | users | table | postgres
public | users_id_seq | sequence | postgres
(3 rows)
hello_dev=# \q
```
If we take a look at the migration generated by `phx.gen.schema` in `priv/repo/migrations/`, we'll see that it will add the columns we specified. It will also add timestamp columns for `inserted_at` and `updated_at` which come from the [`timestamps/1`] function.
```elixir
defmodule Hello.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users) do
add :name, :string
add :email, :string
add :bio, :string
add :number_of_pets, :integer
timestamps(type: :utc_datetime)
end
end
end
```
And here's what that translates to in the actual `users` table.
```console
$ psql
hello_dev=# \d users
Table "public.users"
Column | Type | Modifiers
---------------+--------------------------------+----------------------------------------------------
id | bigint | not null default nextval('users_id_seq'::regclass)
name | character varying(255) |
email | character varying(255) |
bio | character varying(255) |
number_of_pets | integer |
inserted_at | timestamp(0) without time zone | not null
updated_at | timestamp(0) without time zone | not null
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
```
Notice that we do get an `id` column as our primary key by default, even though it isn't listed as a field in our migration.
## Repo configuration
Our `Hello.Repo` module is the foundation we need to work with databases in a Phoenix application. Phoenix generated it for us in `lib/hello/repo.ex`, and this is what it looks like.
```elixir
defmodule Hello.Repo do
use Ecto.Repo,
otp_app: :hello,
adapter: Ecto.Adapters.Postgres
end
```
It begins by defining the repository module. Then it configures our `otp_app` name, and the `adapter` – `Postgres`, in our case.
Our repo has three main tasks - to bring in all the common query functions from [`Ecto.Repo`], to set the `otp_app` name equal to our application name, and to configure our database adapter. We'll talk more about how to use `Hello.Repo` in a bit.
When `phx.new` generated our application, it included some basic repository configuration as well. Let's look at `config/dev.exs`.
```elixir
...
# Configure your database
config :hello, Hello.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "hello_dev",
show_sensitive_data_on_connection_error: true,
pool_size: 10
...
```
We also have similar configuration in `config/test.exs` and `config/runtime.exs` which can also be changed to match your actual credentials.
## The schema
Ecto schemas are responsible for mapping Elixir values to external data sources, as well as mapping external data back into Elixir data structures. We can also define relationships to other schemas in our applications. For example, our `User` schema might have many posts, and each post would belong to a user. Ecto also handles data validation and type casting with changesets, which we'll discuss in a moment.
Here's the `User` schema that Phoenix generated for us.
```elixir
defmodule Hello.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :bio, :string
field :email, :string
field :name, :string
field :number_of_pets, :integer
timestamps(type: :utc_datetime)
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email, :bio, :number_of_pets])
|> validate_required([:name, :email, :bio, :number_of_pets])
end
end
```
Ecto schemas at their core are simply Elixir structs. Our `schema` block is what tells Ecto how to cast our `%User{}` struct fields to and from the external `users` table. Often, the ability to simply cast data to and from the database isn't enough and extra data validation is required. This is where Ecto changesets come in. Let's dive in!
## Changesets and validations
Changesets define a pipeline of transformations our data needs to undergo before it will be ready for our application to use. These transformations might include type-casting, user input validation, and filtering out any extraneous parameters. Often we'll use changesets to validate user input before writing it to the database. Ecto repositories are also changeset-aware, which allows them not only to refuse invalid data, but also perform the minimal database updates possible by inspecting the changeset to know which fields have changed.
Let's take a closer look at our default changeset function.
```elixir
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email, :bio, :number_of_pets])
|> validate_required([:name, :email, :bio, :number_of_pets])
end
```
Right now, we have two transformations in our pipeline. In the first call, we invoke `Ecto.Changeset.cast/3`, passing in our external parameters and marking which fields are required for validation.
[`cast/3`] first takes a struct, then the parameters (the proposed updates), and then the final field is the list of columns to be updated. [`cast/3`] also will only take fields that exist in the schema.
Next, `Ecto.Changeset.validate_required/3` checks that this list of fields is present in the changeset that [`cast/3`] returns. By default with the generator, all fields are required.
We can verify this functionality in `IEx`. Let's fire up our application inside IEx by running `iex -S mix`. In order to minimize typing and make this easier to read, let's alias our `Hello.User` struct.
```console
$ iex -S mix
iex> alias Hello.User
Hello.User
```
Next, let's build a changeset from our schema with an empty `User` struct, and an empty map of parameters.
```elixir
iex> changeset = User.changeset(%User{}, %{})
#Ecto.Changeset<
action: nil,
changes: %{},
errors: [
name: {"can't be blank", [validation: :required]},
email: {"can't be blank", [validation: :required]},
bio: {"can't be blank", [validation: :required]},
number_of_pets: {"can't be blank", [validation: :required]}
],
data: #Hello.User<>,
valid?: false
>
```
Once we have a changeset, we can check if it is valid.
```elixir
iex> changeset.valid?
false
```
Since this one is not valid, we can ask it what the errors are.
```elixir
iex> changeset.errors
[
name: {"can't be blank", [validation: :required]},
email: {"can't be blank", [validation: :required]},
bio: {"can't be blank", [validation: :required]},
number_of_pets: {"can't be blank", [validation: :required]}
]
```
Now, let's make `number_of_pets` optional. In order to do this, we simply remove it from the list in the `changeset/2` function, in `Hello.User`.
```elixir
|> validate_required([:name, :email, :bio])
```
Now casting the changeset should tell us that only `name`, `email`, and `bio` can't be blank. We can test that by running `recompile()` inside IEx and then rebuilding our changeset.
```elixir
iex> recompile()
Compiling 1 file (.ex)
:ok
iex> changeset = User.changeset(%User{}, %{})
#Ecto.Changeset<
action: nil,
changes: %{},
errors: [
name: {"can't be blank", [validation: :required]},
email: {"can't be blank", [validation: :required]},
bio: {"can't be blank", [validation: :required]}
],
data: #Hello.User<>,
valid?: false
>
iex> changeset.errors
[
name: {"can't be blank", [validation: :required]},
email: {"can't be blank", [validation: :required]},
bio: {"can't be blank", [validation: :required]}
]
```
What happens if we pass a key-value pair that is neither defined in the schema nor required?
Inside our existing IEx shell, let's create a `params` map with valid values plus an extra `random_key: "random value"`.
```elixir
iex> params = %{name: "Joe Example", email: "joe@example.com", bio: "An example to all", number_of_pets: 5, random_key: "random value"}
%{
bio: "An example to all",
email: "joe@example.com",
name: "Joe Example",
number_of_pets: 5,
random_key: "random value"
}
```
Next, let's use our new `params` map to create another changeset.
```elixir
iex> changeset = User.changeset(%User{}, params)
#Ecto.Changeset<
action: nil,
changes: %{
bio: "An example to all",
email: "joe@example.com",
name: "Joe Example",
number_of_pets: 5
},
errors: [],
data: #Hello.User<>,
valid?: true
>
```
Our new changeset is valid.
```elixir
iex> changeset.valid?
true
```
We can also check the changeset's changes - the map we get after all of the transformations are complete.
```elixir
iex(9)> changeset.changes
%{bio: "An example to all", email: "joe@example.com", name: "Joe Example",
number_of_pets: 5}
```
Notice that our `random_key` key and `"random_value"` value have been removed from the final changeset. Changesets allow us to cast external data, such as user input on a web form or data from a CSV file into valid data into our system. Invalid parameters will be stripped and bad data that is unable to be cast according to our schema will be highlighted in the changeset errors.
We can validate more than just whether a field is required or not. Let's take a look at some finer-grained validations.
What if we had a requirement that all biographies in our system must be at least two characters long? We can do this easily by adding another transformation to the pipeline in our changeset which validates the length of the `bio` field.
```elixir
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email, :bio, :number_of_pets])
|> validate_required([:name, :email, :bio, :number_of_pets])
|> validate_length(:bio, min: 2)
end
```
Now, if we try to cast data containing a value of `"A"` for our user's `bio`, we should see the failed validation in the changeset's errors.
```elixir
iex> recompile()
iex> changeset = User.changeset(%User{}, %{bio: "A"})
iex> changeset.errors[:bio]
{"should be at least %{count} character(s)",
[count: 2, validation: :length, kind: :min, type: :string]}
```
If we also have a requirement for the maximum length that a bio can have, we can simply add another validation.
```elixir
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email, :bio, :number_of_pets])
|> validate_required([:name, :email, :bio, :number_of_pets])
|> validate_length(:bio, min: 2)
|> validate_length(:bio, max: 140)
end
```
Let's say we want to perform at least some rudimentary format validation on the `email` field. All we want to check for is the presence of the `@`. The `Ecto.Changeset.validate_format/3` function is just what we need.
```elixir
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email, :bio, :number_of_pets])
|> validate_required([:name, :email, :bio, :number_of_pets])
|> validate_length(:bio, min: 2)
|> validate_length(:bio, max: 140)
|> validate_format(:email, ~r/@/)
end
```
If we try to cast a user with an email of `"example.com"`, we should see an error message like the following:
```elixir
iex> recompile()
iex> changeset = User.changeset(%User{}, %{email: "example.com"})
iex> changeset.errors[:email]
{"has invalid format", [validation: :format]}
```
There are many more validations and transformations we can perform in a changeset. Please see the [Ecto Changeset documentation](https://hexdocs.pm/ecto/Ecto.Changeset.html) for more information.
## Data persistence
We've explored migrations and schemas, but we haven't yet persisted any of our schemas or changesets. We briefly looked at our repository module in `lib/hello/repo.ex` earlier, and now it's time to put it to use.
Ecto repositories are the interface into a storage system, be it a database like PostgreSQL or an external service like a RESTful API. The `Repo` module's purpose is to take care of the finer details of persistence and data querying for us. As the caller, we only care about fetching and persisting data. The `Repo` module takes care of the underlying database adapter communication, connection pooling, and error translation for database constraint violations.
Let's head back over to IEx with `iex -S mix`, and insert a couple of users into the database.
```elixir
iex> alias Hello.{Repo, User}
[Hello.Repo, Hello.User]
iex> Repo.insert(%User{email: "user1@example.com"})
[debug] QUERY OK db=6.5ms queue=0.5ms idle=1358.3ms
INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user1@example.com", ~U[2021-02-25 01:58:55Z], ~U[2021-02-25 01:58:55Z]]
{:ok,
%Hello.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
bio: nil,
email: "user1@example.com",
id: 1,
inserted_at: ~U[2021-02-25 01:58:55Z],
name: nil,
number_of_pets: nil,
updated_at: ~U[2021-02-25 01:58:55Z]
}}
iex> Repo.insert(%User{email: "user2@example.com"})
[debug] QUERY OK db=1.3ms idle=1402.7ms
INSERT INTO "users" ("email","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user2@example.com", ~U[2021-02-25 02:03:28Z], ~U[2021-02-25 02:03:28Z]]
{:ok,
%Hello.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
bio: nil,
email: "user2@example.com",
id: 2,
inserted_at: ~U[2021-02-25 02:03:28Z],
name: nil,
number_of_pets: nil,
updated_at: ~U[2021-02-25 02:03:28Z]
}}
```
We started by aliasing our `User` and `Repo` modules for easy access. Next, we called [`Repo.insert/2`] with a User struct. Since we are in the `dev` environment, we can see the debug logs for the query our repository performed when inserting the underlying `%User{}` data. We received a two-element tuple back with `{:ok, %User{}}`, which lets us know the insertion was successful.
We could also insert a user by passing a changeset to [`Repo.insert/2`]. If the changeset is valid, the repository will use an optimized database query to insert the record, and return a two-element tuple back, as above. If the changeset is not valid, we receive a two-element tuple consisting of `:error` plus the invalid changeset.
With a couple of users inserted, let's fetch them back out of the repo.
```elixir
iex> Repo.all(User)
[debug] QUERY OK source="users" db=5.8ms queue=1.4ms idle=1672.0ms
SELECT u0."id", u0."bio", u0."email", u0."name", u0."number_of_pets", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
[
%Hello.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
bio: nil,
email: "user1@example.com",
id: 1,
inserted_at: ~U[2021-02-25 01:58:55Z],
name: nil,
number_of_pets: nil,
updated_at: ~U[2021-02-25 01:58:55Z]
},
%Hello.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
bio: nil,
email: "user2@example.com",
id: 2,
inserted_at: ~U[2021-02-25 02:03:28Z],
name: nil,
number_of_pets: nil,
updated_at: ~U[2021-02-25 02:03:28Z]
}
]
```
That was easy! `Repo.all/1` takes a data source, our `User` schema in this case, and translates that to an underlying SQL query against our database. After it fetches the data, the Repo then uses our Ecto schema to map the database values back into Elixir data structures according to our `User` schema. We're not just limited to basic querying – Ecto includes a full-fledged query DSL for advanced SQL generation. In addition to a natural Elixir DSL, Ecto's query engine gives us multiple great features, such as SQL injection protection and compile-time optimization of queries. Let's try it out.
```elixir
iex> import Ecto.Query
Ecto.Query
iex> Repo.all(from u in User, select: u.email)
[debug] QUERY OK source="users" db=0.8ms queue=0.9ms idle=1634.0ms
SELECT u0."email" FROM "users" AS u0 []
["user1@example.com", "user2@example.com"]
```
First, we imported `Ecto.Query`, which imports the [`from/2`] macro of Ecto's Query DSL. Next, we built a query which selects all the email addresses in our users table. Let's try another example.
```elixir
iex> Repo.one(from u in User, where: ilike(u.email, "%1%"),
select: count(u.id))
[debug] QUERY OK source="users" db=1.6ms SELECT count(u0."id") FROM "users" AS u0 WHERE (u0."email" ILIKE '%1%') []
1
```
Now we're starting to get a taste of Ecto's rich querying capabilities. We used [`Repo.one/2`] to fetch the count of all users with an email address containing `1`, and received the expected count in return. This just scratches the surface of Ecto's query interface, and much more is supported such as sub-querying, interval queries, and advanced select statements. For example, let's build a query to fetch a map of all user id's to their email addresses.
```elixir
iex> Repo.all(from u in User, select: %{u.id => u.email})
[debug] QUERY OK source="users" db=0.9ms
SELECT u0."id", u0."email" FROM "users" AS u0 []
[
%{1 => "user1@example.com"},
%{2 => "user2@example.com"}
]
```
That little query packed a big punch. It both fetched all user emails from the database and efficiently built a map of the results in one go. You should browse the [Ecto.Query documentation](https://hexdocs.pm/ecto/Ecto.Query.html#content) to see the breadth of supported query features.
In addition to inserts, we can also perform updates and deletes with [`Repo.update/2`] and [`Repo.delete/2`] to update or delete a single schema. Ecto also supports bulk persistence with the [`Repo.insert_all/3`], [`Repo.update_all/3`], and [`Repo.delete_all/2`] functions.
There is quite a bit more that Ecto can do and we've only barely scratched the surface. With a solid Ecto foundation in place, we're now ready to continue building our app and integrate the web-facing application with our backend persistence. Along the way, we'll expand our Ecto knowledge and learn how to properly isolate our web interface from the underlying details of our system. Please take a look at the [Ecto documentation](https://hexdocs.pm/ecto/) for the rest of the story.
In our [Data modelling guides](contexts.html), we'll find out how to wrap up our Ecto access and business logic behind modules that group related functionality. We'll see how Phoenix helps us design maintainable applications, and we'll find out about other neat Ecto features along the way.
## Mix tasks
Ecto comes with a collection of Mix tasks to make it easier to manage your database and your application. Here is a quick look into the most important ones.
### `mix ecto.create`
This task will create the database specified by our application repositories, but we can pass in another repo if we want.
Here's what it looks like in action.
```console
$ mix ecto.create
The database for Hello.Repo has been created.
```
There are a few things that can go wrong with `ecto.create`. If our Postgres database doesn't have a "postgres" role (user), we'll get an error like this one.
```console
$ mix ecto.create
** (Mix) The database for Hello.Repo couldn't be created, reason given: psql: FATAL: role "postgres" does not exist
```
We can fix this by creating the "postgres" role in the `psql` console with the permissions needed to log in and create a database.
```console
=# CREATE ROLE postgres LOGIN CREATEDB;
CREATE ROLE
```
If the "postgres" role does not have permission to log in to the application, we'll get this error.
```console
$ mix ecto.create
** (Mix) The database for Hello.Repo couldn't be created, reason given: psql: FATAL: role "postgres" is not permitted to log in
```
To fix this, we need to change the permissions on our "postgres" user to allow login.
```console
=# ALTER ROLE postgres LOGIN;
ALTER ROLE
```
If the "postgres" role does not have permission to create a database, we'll get this error.
```console
$ mix ecto.create
** (Mix) The database for Hello.Repo couldn't be created, reason given: ERROR: permission denied to create database
```
To fix this, we need to change the permissions on our "postgres" user in the `psql` console to allow database creation.
```console
=# ALTER ROLE postgres CREATEDB;
ALTER ROLE
```
If the "postgres" role is using a password different from the default "postgres", we'll get this error.
```console
$ mix ecto.create
** (Mix) The database for Hello.Repo couldn't be created, reason given: psql: FATAL: password authentication failed for user "postgres"
```
To fix this, we can change the password in the environment specific configuration file. For the development environment the password used can be found at the bottom of the `config/dev.exs` file.
Finally, if we happen to have another repo called `OurCustom.Repo` that we want to create the database for, we can run this.
```console
$ mix ecto.create -r OurCustom.Repo
The database for OurCustom.Repo has been created.
```
### `mix ecto.drop`
This task will drop the database specified in our repo. By default it will look for the repo named after our application (the one generated with our app unless we opted out of Ecto). It will not prompt us to check if we're sure we want to drop the database, so do exercise caution.
```console
$ mix ecto.drop
The database for Hello.Repo has been dropped.
```
### `mix ecto.gen.migration`
Migrations are a programmatic, repeatable way to affect changes to a database schema. Phoenix generators take care of generating migrations for us whenever we create a new context or schema, but if you want to generate a migration from scratch, `mix ecto.gen.migration` has our back. Let's see an example.
We simply need to invoke the task with a `snake_case` version of the module name that we want. Preferably, the name will describe what we want the migration to do.
```console
$ mix ecto.gen.migration add_comments_table
* creating priv/repo/migrations
* creating priv/repo/migrations/20150318001628_add_comments_table.exs
```
Notice that the migration's filename begins with a string representation of the date and time the file was created.
Let's take a look at the file `ecto.gen.migration` has generated for us at `priv/repo/migrations/20150318001628_add_comments_table.exs`.
```elixir
defmodule Hello.Repo.Migrations.AddCommentsTable do
use Ecto.Migration
def change do
end
end
```
Notice that there is a single function `change/0` which will handle both forward migrations and rollbacks. We'll define the schema changes that we want using Ecto's handy DSL, and Ecto will figure out what to do depending on whether we are rolling forward or rolling back. Very nice indeed.
What we want to do is create a `comments` table with a `body` column, a `word_count` column, and timestamp columns for `inserted_at` and `updated_at`.
```elixir
...
def change do
create table(:comments) do
add :body, :string
add :word_count, :integer
timestamps(type: :utc_datetime)
end
end
...
```
For more information on how to modify your database schema please refer to the
[Ecto's migration DSL docs](https://hexdocs.pm/ecto_sql/Ecto.Migration.html).
For example, to alter an existing schema see the documentation on Ecto’s
[`alter/2`](`Ecto.Migration.alter/2`) function.
That's it! We're ready to run our migration.
### `mix ecto.migrate`
Once we have our migration module ready, we can simply run `mix ecto.migrate` to have our changes applied to the database. We have already used it earlier in this chapter, but let's take it for a spin once more for our newly generated migration.
```console
$ mix ecto.migrate
[info] == Running Hello.Repo.Migrations.AddCommentsTable.change/0 forward
[info] create table comments
[info] == Migrated in 0.1s
```
When we first run `ecto.migrate`, it will create a table for us called `schema_migrations`. This will keep track of all the migrations which we run by storing the timestamp portion of the migration's filename.
Here's what the `schema_migrations` table looks like.
```console
hello_dev=# select * from schema_migrations;
version | inserted_at
---------------+---------------------
20250317170448 | 2025-03-17 21:07:26
20250318001628 | 2025-03-18 01:45:00
(2 rows)
```
When we roll back a migration, `mix ecto.rollback`, to be discussed next, we will remove the record representing this migration from `schema_migrations`.
By default, `ecto.migrate` will execute all pending migrations. We can exercise more control over which migrations we run by specifying some options when we run the task.
We can specify the number of pending migrations we would like to run with the `-n` or `--step` options.
```console
$ mix ecto.migrate -n 2
[info] == Running Hello.Repo.Migrations.CreatePost.change/0 forward
[info] create table posts
[info] == Migrated in 0.0s
[info] == Running Hello.Repo.Migrations.AddCommentsTable.change/0 forward
[info] create table comments
[info] == Migrated in 0.0s
```
The `--step` option will behave the same way.
```console
$ mix ecto.migrate --step 2
```
The `--to` option will run all migrations up to and including given version.
```console
$ mix ecto.migrate --to 20150317170448
```
### `mix ecto.rollback`
The `mix ecto.rollback` task will reverse the last migration we have run, undoing the schema changes. `ecto.migrate` and `ecto.rollback` are mirror images of each other.
```console
$ mix ecto.rollback
[info] == Running Hello.Repo.Migrations.AddCommentsTable.change/0 backward
[info] drop table comments
[info] == Migrated in 0.0s
```
`ecto.rollback` will handle the same options as `ecto.migrate`, so `-n`, `--step`, `-v`, and `--to` will behave as they do for `ecto.migrate`.
[`cast/3`]: `Ecto.Changeset.cast/3`
[`from/2`]: `Ecto.Query.from/2`
[`Repo.delete_all/2`]: `c:Ecto.Repo.delete_all/2`
[`Repo.delete/2`]: `c:Ecto.Repo.delete/2`
[`Repo.insert_all/3`]: `c:Ecto.Repo.insert_all/3`
[`Repo.insert/2`]: `c:Ecto.Repo.insert/2`
[`Repo.one/2`]: `c:Ecto.Repo.one/2`
[`Repo.update_all/3`]: `c:Ecto.Repo.update_all/3`
[`Repo.update/2`]: `c:Ecto.Repo.update/2`
[`timestamps/1`]: `Ecto.Migration.timestamps/1`
================================================
FILE: guides/howto/custom_error_pages.md
================================================
# Custom Error Pages
New Phoenix projects have two error views called `ErrorHTML` and `ErrorJSON`, which live in `lib/hello_web/controllers/`. The purpose of these views is to handle errors in a general way for each format, from one centralized location.
## The Error Views
For new applications, the `ErrorHTML` and `ErrorJSON` views looks like this:
```elixir
defmodule HelloWeb.ErrorHTML do
use HelloWeb, :html
# If you want to customize your error pages,
# uncomment the embed_templates/1 call below
# and add pages to the error directory:
#
# * lib/<%= @lib_web_name %>/controllers/error_html/404.html.heex
# * lib/<%= @lib_web_name %>/controllers/error_html/500.html.heex
#
# embed_templates "error_html/*"
# The default is to render a plain text page based on
# the template name. For example, "404.html" becomes
# "Not Found".
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
end
defmodule HelloWeb.ErrorJSON do
# If you want to customize a particular status code,
# you may add your own clauses, such as:
#
# def render("500.json", _assigns) do
# %{errors: %{detail: "Internal Server Error"}}
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.json" becomes
# "Not Found".
def render(template, _assigns) do
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
end
end
```
Before we dive into this, let's see what the rendered `404 Not Found` message looks like in a browser. In the development environment, Phoenix will debug errors by default, showing us a very informative debugging page. What we want here, however, is to see what page the application would serve in production. In order to do that, we need to set `debug_errors: false` in `config/dev.exs`.
```elixir
import Config
config :hello, HelloWeb.Endpoint,
...,
debug_errors: false,
code_reloader: true,
...
```
After modifying our config file, we need to restart our server in order for this change to take effect. After restarting the server, let's go to [http://localhost:4000/such/a/wrong/path](http://localhost:4000/such/a/wrong/path) for a running local application and see what we get.
Ok, that's not very exciting. We get the bare string "Not Found", displayed without any markup or styling.
The first question is, where does that error string come from? The answer is right in `ErrorHTML`.
```elixir
def render(template, _assigns) do
Phoenix.Controller.status_message_from_template(template)
end
```
Great, so we have this `render/2` function that takes a template and an `assigns` map, which we ignore. When you call `render(conn, :some_template)` from the controller, Phoenix first looks for a `some_template/1` function on the view module. If no function exists, it falls back to calling `render/2` with the template and format name, such as `"some_template.html"`.
In other words, to provide custom error pages, we could simply define a proper `render/2` function clause in `HelloWeb.ErrorHTML`.
```elixir
def render("404.html", _assigns) do
"Page Not Found"
end
```
But we can do even better.
Phoenix generates an `ErrorHTML` for us, but it doesn't give us a `lib/hello_web/controllers/error_html` directory. Let's create one now. Inside our new directory, let's add a template named `404.html.heex` and give it some markup – a mixture of our application layout and a new `
` with our message to the user.
```heex
Welcome to Phoenix!
Sorry, the page you are looking for does not exist.
```
After you define the template file, remember to remove the equivalent `render/2` clause for that template, as otherwise the function overrides the template. Let's do so for the 404.html clause we have previously introduced in `lib/hello_web/controllers/error_html.ex`. We also need to tell Phoenix to embed our templates into the module:
```diff
+ embed_templates "error_html/*"
- def render("404.html", _assigns) do
- "Page Not Found"
- end
```
Now, when we go back to [http://localhost:4000/such/a/wrong/path](http://localhost:4000/such/a/wrong/path), we should see a much nicer error page. It is worth noting that we did not render our `404.html.heex` template through our application layout, even though we want our error page to have the look and feel of the rest of our site. This is to avoid circular errors. For example, what happens if our application failed due to an error in the layout? Attempting to render the layout again will just trigger another error. So ideally we want to minimize the amount of dependencies and logic in our error templates, sharing only what is necessary.
## Custom exceptions
Elixir provides a macro called `defexception/1` for defining custom exceptions. Exceptions are represented as structs, and structs need to be defined inside of modules.
In order to create a custom exception, we need to define a new module. Conventionally, this will have "Error" in the name. Inside that module, we need to define a new exception with `defexception/1`, the file `lib/hello_web.ex` seems like a good place for it.
```elixir
defmodule HelloWeb.SomethingNotFoundError do
defexception [:message]
end
```
You can raise your new exception like this:
```elixir
raise HelloWeb.SomethingNotFoundError, "oops"
```
By default, Plug and Phoenix will treat all exceptions as 500 errors. However, Plug provides a protocol called `Plug.Exception` where we are able to customize the status and add actions that exception structs can return on the debug error page.
If we wanted to supply a status of 404 for an `HelloWeb.SomethingNotFoundError` error, we could do it by defining an implementation for the `Plug.Exception` protocol like this, in `lib/hello_web.ex`:
```elixir
defimpl Plug.Exception, for: HelloWeb.SomethingNotFoundError do
def status(_exception), do: 404
def actions(_exception), do: []
end
```
Alternatively, you could define a `plug_status` field directly in the exception struct:
```elixir
defmodule HelloWeb.SomethingNotFoundError do
defexception [:message, plug_status: 404]
end
```
However, implementing the `Plug.Exception` protocol by hand can be convenient in certain occasions, such as when providing actionable errors.
## Actionable errors
Exception actions are functions that can be triggered from the error page, and they're basically a list of maps defining a `label` and a `handler` to be executed. As an example, Phoenix will display an error if you have pending migrations and will provide a button on the error page to perform the pending migrations.
When `debug_errors` is `true`, they are rendered in the error page as a collection of buttons and follow the format of:
```elixir
[
%{
label: String.t(),
handler: {module(), function :: atom(), args :: []}
}
]
```
If we wanted to return some actions for an `HelloWeb.SomethingNotFoundError` we would implement `Plug.Exception` like this:
```elixir
defimpl Plug.Exception, for: HelloWeb.SomethingNotFoundError do
def status(_exception), do: 404
def actions(_exception) do
[
%{
label: "Run seeds",
handler: {Code, :eval_file, ["priv/repo/seeds.exs"]}
}
]
end
end
```
================================================
FILE: guides/howto/file_uploads.md
================================================
# File Uploads
One common task for web applications is uploading files. These files might be images, videos, PDFs, or files of any other type. In order to upload files through an HTML interface, we need a `file` input tag in a multipart form.
> #### Looking for the LiveView Uploads guide? {: .neutral}
>
> This guide explains multipart HTTP file uploads via `Plug.Upload`.
> For more information about LiveView file uploads, including direct-to-cloud external uploads on
> the client, refer to the [LiveView Uploads guide](https://hexdocs.pm/phoenix_live_view/uploads.html).
Plug provides a `Plug.Upload` struct to hold the data from the `file` input. A `Plug.Upload` struct will automatically appear in your request parameters if a user has selected a file when they submit the form.
In this guide you will do the following:
1. Configure a multipart form
2. Add a file input element to the form
3. Verify your upload params
4. Manage your uploaded files
In the [`Contexts guide`](contexts.md), we generated an HTML resource for products. We can reuse the form we generated there in order to demonstrate how file uploads work in Phoenix. Please refer to that guide for instructions on generating the product resource you will be using here.
### Configure a multipart form
The first thing you need to do is change your form into a multipart form. The `HelloWeb.CoreComponents` `form/1` component accepts a `multipart` attribute where you can specify this.
Here is the form from `lib/hello_web/controllers/product_html/product_form.html.heex` with that change in place:
```heex
<.form :let={f} for={@changeset} action={@action} multipart>
...
```
### Add a file input
Once you have a multipart form, you need a `file` input. Here's how you would do that, also in `product_form.html.heex`:
```heex
...
<.input field={f[:photo]} type="file" label="Photo" />
<.button>Save Product
```
When rendered, here is the HTML for the default `HelloWeb.CoreComponents` `input/1` component:
```html
```
Note the `name` attribute of your `file` input. This will create the `"photo"` key in the `product_params` map which will be available in your controller action.
This is all from the form side. Now when users submit the form, a `POST` request will route to your `HelloWeb.ProductController` `create/2` action.
> #### Should I add photo to my Ecto schema? {: .neutral}
>
> The photo input does not need to be part of your schema for it to come across in the `product_params`. If you want to persist any properties of the photo in a database, however, you would need to add it to your `Hello.Product` schema.
### Verify your upload params
Since you generated an HTML resource, you can now start your server with `mix phx.server`, visit [http://localhost:4000/products/new](http://localhost:4000/products/new), and create a new product with a photo.
Before you begin, add `IO.inspect product_params` to the top of your `ProductController.create/2` action in `lib/hello_web/controllers/product_controller.ex`. This will show the `product_params` in your development log so you can get a better sense of what's happening.
```elixir
...
def create(conn, %{"product" => product_params}) do
IO.inspect product_params
...
```
When you do that, this is what your `product_params` will output in the log:
```elixir
%{"title" => "Metaprogramming Elixir", "description" => "Write Less Code, Get More Done (and Have Fun!)", "price" => "15.000000", "views" => "0",
"photo" => %Plug.Upload{content_type: "image/png", filename: "meta-cover.png", path: "/var/folders/_6/xbsnn7tx6g9dblyx149nrvbw0000gn/T//plug-1434/multipart-558399-917557-1"}}
```
You have a `"photo"` key which maps to the pre-populated `Plug.Upload` struct representing your uploaded photo.
To make this easier to read, focus on the struct itself:
```elixir
%Plug.Upload{content_type: "image/png", filename: "meta-cover.png", path: "/var/folders/_6/xbsnn7tx6g9dblyx149nrvbw0000gn/T//plug-1434/multipart-558399-917557-1"}
```
`Plug.Upload` provides the file's content type, original filename, and path to the temporary file which Plug created for you. In this case, `"/var/folders/_6/xbsnn7tx6g9dblyx149nrvbw0000gn/T//plug-1434/"` is the directory created by Plug in which to put uploaded files. The directory will persist across requests. `"multipart-558399-917557-1"` is the name Plug gave to your uploaded file. If you had multiple `file` inputs and if the user selected photos for all of them, you would have multiple files scattered in temporary directories. Plug will make sure all the filenames are unique.
> #### Plug.Upload files are temporary {: .info}
>
> Plug removes uploads from its directory as the request completes. If you need to do anything with this file, you need to do it before then (or [give it away](`Plug.Upload.give_away/3`), but that is outside the scope of this guide).
### Manage your uploaded files
Once you have the `Plug.Upload` struct available in your controller, you can perform any operation on it you want. For example, you may want to do one or more of the following:
* Check to make sure the file exists with `File.exists?/1`
* Copy the file somewhere else on the filesystem with `File.cp/2`
* Give the file away to another Elixir process with `Plug.Upload.give_away/3`
* Send it to S3 with an external library
* Send it back to the client with `Plug.Conn.send_file/5`
In a production system, you may want to copy the file to a root directory, such as `/media`. When doing so, it is important to guarantee the names are unique. For instance, if you are allowing users to upload product cover images, you could use the product id to generate a unique name:
```elixir
if upload = product_params["photo"] do
extension = Path.extname(upload.filename)
File.cp(upload.path, "/media/#{product.id}-cover#{extension}")
end
```
Then a `Plug.Static` plug could be added in your `lib/my_app_web/endpoint.ex` to serve the files at `"/media"`:
```elixir
plug Plug.Static, at: "/uploads", from: "/media"
```
The uploaded file can now be accessed from your browsers using a path such as `"/uploads/1-cover.jpg"`. In practice, there are other concerns you want to handle when uploading files, such validating extensions, encoding names, and so on. Many times, using a library that already handles such cases is preferred.
Finally, notice that when there is no data from the `file` input, you get neither the `"photo"` key nor a `Plug.Upload` struct. Here are the `product_params` from the log.
```elixir
%{"title" => "Metaprogramming Elixir", "description" => "Write Less Code, Get More Done (and Have Fun!)", "price" => "15.000000", "views" => "0"}
```
## Configuring upload limits
The conversion from the data being sent by the form to an actual `Plug.Upload` is done by the `Plug.Parsers` plug which you can find inside `HelloWeb.Endpoint`:
```elixir
# lib/hello_web/endpoint.ex
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
```
Besides the options above, `Plug.Parsers` accepts other options to control data upload:
* `:length` - sets the max body length to read, defaults to `8_000_000` bytes
* `:read_length` - set the amount of bytes to read at one time, defaults to `1_000_000` bytes
* `:read_timeout` - set the timeout for each chunk received, defaults to `15_000` ms
The first option configures the maximum data allowed. The remaining ones configure how much data we expect to read and its frequency. If the client cannot push data fast enough, the connection will be terminated. Phoenix ships with reasonable defaults but you may want to customize it under special circumstances, for example, if you are expecting really slow clients to send large chunks of data.
It is also worth pointing out those limits are important as a security mechanism. For example, if you don't set a limit for data upload, attackers could open up thousands of connections to your application and send one byte every 2 minutes, which would take very long to complete while using up all connections to your server. The limits above expect at least a reasonable amount of progress, making attackers' lives a bit harder.
================================================
FILE: guides/howto/swapping_databases.md
================================================
# Swapping Databases
Phoenix applications are configured to use PostgreSQL by default, but what if we want to use another database, such as MySQL? In this guide, we'll walk through changing that default whether we are about to create a new application, or whether we have an existing one configured for PostgreSQL.
## Using `phx.new`
If we are about to create a new application, configuring our application to use MySQL is easy. We can simply pass the `--database mysql` flag to `phx.new` and everything will be configured correctly.
```console
$ mix phx.new hello_phoenix --database mysql
```
This will set up all the correct dependencies and configuration for us automatically. Once we install those dependencies with `mix deps.get`, we'll be ready to begin working with Ecto in our application.
## Within an existing application
If we have an existing application, all we need to do is switch adapters and make some small configuration changes.
To switch adapters, we need to remove the Postgrex dependency and add a new one for MyXQL instead.
Let's open up our `mix.exs` file and do that now.
```elixir
defmodule HelloPhoenix.MixProject do
use Mix.Project
...
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.4.0"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.13"},
{:myxql, ">= 0.0.0"},
...
]
end
end
```
Next, we need to configure our adapter to use the default MySQL credentials by updating `config/dev.exs`:
```elixir
config :hello_phoenix, HelloPhoenix.Repo,
username: "root",
password: "",
database: "hello_phoenix_dev"
```
If we have an existing configuration block for our `HelloPhoenix.Repo`, we can simply change the values to match our new ones. You also need to configure the correct values in the `config/test.exs` and `config/runtime.exs` files as well.
The last change is to open up `lib/hello_phoenix/repo.ex` and make sure to set the `:adapter` to `Ecto.Adapters.MyXQL`.
Now all we need to do is fetch our new dependency, and we'll be ready to go.
```console
$ mix deps.get
```
With our new adapter installed and configured, we're ready to create our database.
```console
$ mix ecto.create
```
The database for HelloPhoenix.Repo has been created.
We're also ready to run any migrations, or do anything else with Ecto that we may choose.
```console
$ mix ecto.migrate
[info] == Running HelloPhoenix.Repo.Migrations.CreateUser.change/0 forward
[info] create table users
[info] == Migrated in 0.2s
```
## Other options
While Phoenix uses the `Ecto` project to interact with the data access layer, there are many other data access options, some even built into the Erlang standard library. [ETS](https://www.erlang.org/doc/man/ets.html) – available in Ecto via [`etso`](https://hexdocs.pm/etso/) – and [DETS](https://www.erlang.org/doc/man/dets.html) are key-value data stores built into [Erlang/OTP](https://www.erlang.org/doc/). Both Elixir and Erlang also have a number of libraries for working with a wide range of popular data stores.
The data world is your oyster, but we won't be covering these options in these guides.
================================================
FILE: guides/howto/using_ssl.md
================================================
# Using SSL
To prepare an application to serve requests over SSL, we need to add a little bit of configuration and two environment variables. In order for SSL to actually work, we'll need a key file and certificate file from a certificate authority. The environment variables that we'll need are paths to those two files.
The configuration consists of a new `https:` key for our endpoint whose value is a keyword list of port, path to the key file, and path to the cert (PEM) file. If we add the `otp_app:` key whose value is the name of our application, Plug will begin to look for them at the root of our application. We can then put those files in our `priv` directory and set the paths to `priv/our_keyfile.key` and `priv/our_cert.crt`.
Here's an example configuration from `config/runtime.exs`.
```elixir
import Config
config :hello, HelloWeb.Endpoint,
http: [port: String.to_integer(System.get_env("PORT"))],
url: [host: "example.com"],
cache_static_manifest: "priv/static/cache_manifest.json",
https: [
port: 443,
cipher_suite: :strong,
otp_app: :hello,
keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
certfile: System.get_env("SOME_APP_SSL_CERT_PATH"),
# OPTIONAL Key for intermediate certificates:
cacertfile: System.get_env("INTERMEDIATE_CERTFILE_PATH")
]
```
Without the `otp_app:` key, we need to provide absolute paths to the files wherever they are on the filesystem in order for Plug to find them.
```elixir
Path.expand("../../../some/path/to/ssl/key.pem", __DIR__)
```
The options under the `https:` key are passed to the Plug adapter, typically `Bandit`, which in turn uses `Plug.SSL` to select the TLS socket options. Please refer to the documentation for [Plug.SSL.configure/1](https://hexdocs.pm/plug/Plug.SSL.html#configure/1) for more information on the available options and their defaults. The [Plug HTTPS Guide](https://hexdocs.pm/plug/https.html) and the [Erlang/OTP ssl](https://www.erlang.org/doc/man/ssl.html) documentation also provide valuable information.
## SSL in Development
If you would like to use HTTPS in development, a self-signed certificate can be generated by running: `mix phx.gen.cert`. This requires Erlang/OTP 20 or later.
With your self-signed certificate, your development configuration in `config/dev.exs` can be updated to run an HTTPS endpoint:
```elixir
config :my_app, MyAppWeb.Endpoint,
...
https: [
port: 4001,
cipher_suite: :strong,
keyfile: "priv/cert/selfsigned_key.pem",
certfile: "priv/cert/selfsigned.pem"
]
```
This can replace your `http` configuration, or you can run HTTP and HTTPS servers on different ports.
## Force SSL
In many cases, you'll want to force all incoming requests to use SSL by redirecting HTTP to HTTPS. This can be accomplished by setting the `:force_ssl` option in your endpoint configuration. It expects a list of options which are forwarded to `Plug.SSL`. By default, it sets the "strict-transport-security" header in HTTPS requests, forcing browsers to always use HTTPS. For example:
```elixir
config :my_app, MyAppWeb.Endpoint,
force_ssl: [rewrite_on: [:x_forwarded_proto]]
```
If an unsafe (HTTP) request is sent, the above will redirect to the HTTPS version using the `:host` specified in the `:url` configuration. The `rewrite_on:` key specifies the HTTP header used by a reverse proxy or load balancer in front of the application to indicate whether the request was received over HTTP or HTTPS.
To dynamically redirect to the `host` of the current request, set `:host` in the `:force_ssl` configuration to `nil`. In such cases, you must also set `x_forwarded_host` and `x_forwarded_port` if running behind a proxy:
```elixir
config :my_app, MyAppWeb.Endpoint,
force_ssl: [rewrite_on: [:x_forwarded_proto, :x_forwarded_host, :x_forwarded_port], host: nil]
```
Furthermore, keep in mind `force_ssl` will redirect all requests, except the ones coming from localhost. If your application is doing probeness checks using another origin, such as "127.0.0.1" or an internal IP address, you may need to explicitly exclude them from `force_ssl`:
```elixir
config :my_app, MyAppWeb.Endpoint,
force_ssl: [rewrite_on: [:x_forwarded_proto], exclude: ["localhost", "127.0.0.1"]]
```
It is important to note that `force_ssl:` is a *compile* time config, so it normally is set in `prod.exs`, it will not work when set from `runtime.exs`.
For more information on the implications of offloading TLS to an external element, in particular relating to secure cookies, refer to the [Plug HTTPS Guide](https://hexdocs.pm/plug/https.html#offloading-tls). Keep in mind that the options passed to `Plug.SSL` in that document should be set using the `force_ssl:` endpoint option in a Phoenix application.
## HSTS
HSTS, short for 'HTTP Strict-Transport-Security', is a mechanism that allows websites to declare themselves as accessible exclusively through a secure connection (HTTPS). It was introduced to prevent man-in-the-middle attacks that strip SSL/TLS encryption. HSTS causes web browsers to redirect from HTTP to HTTPS and to refuse to connect unless the connection uses SSL/TLS.
With `force_ssl: [hsts: true]` set, the `Strict-Transport-Security` header is added with a max-age that defines the duration for which the policy is valid. Modern web browsers will respond to this by redirecting from HTTP to HTTPS, among other consequences. [RFC6797](https://tools.ietf.org/html/rfc6797), which defines HSTS, also specifies that **the browser should keep track of a host's policy and apply it until it expires.** It further specifies that **traffic on any port other than 80 is assumed to be encrypted** as per the policy.
While HSTS is recommended in production, it can lead to unexpected behavior when accessing applications on localhost. For instance, accessing an application with HSTS enabled at `https://localhost:4000` leads to a situation where all subsequent traffic from localhost, except for port 80, is expected to be encrypted. This can disrupt traffic to other local servers or proxies running on your computer that are unrelated to your Phoenix application and may not support encrypted traffic.
If you inadvertently enable HSTS for localhost, you may need to reset your browser's cache before it will accept HTTP traffic from localhost again.
For Chrome:
1. Open the Developer Tools Panel.
2. Click and hold the reload icon next to the address bar to reveal a dropdown menu.
3. Select "Empty Cache and Hard Reload".
For Safari:
1. Clear your browser cache.
2. Remove the entry from `~/Library/Cookies/HSTS.plist` or delete the file entirely.
3. Restart Safari.
For other browsers, please consult the documentation for HSTS.
Alternatively, setting the `:expires` option on `force_ssl` to `0` should expire the entry and disable HSTS.
For more information on HSTS options, see [Plug.SSL](https://hexdocs.pm/plug/Plug.SSL.html).
================================================
FILE: guides/howto/writing_a_channels_client.md
================================================
# Writing a Channels Client
Client libraries for Phoenix Channels already exist in [several languages](https://hexdocs.pm/phoenix/channels.html#client-libraries), but if you want to write your own, this guide should get you started.
It may also be useful as a guide for manual testing with a WebSocket client.
## Overview
Because WebSockets are bidirectional, messages can flow in either direction at any time.
For this reason, clients typically use callbacks to handle incoming messages whenever they come.
A client must join at least one topic to begin sending and receiving messages, and may join any number of topics using the same connection.
## Connecting
To establish a WebSocket connection to Phoenix Channels, first make note of the `socket` declaration in the application's `Endpoint` module.
For example, if you see: `socket "/mobile", MyAppWeb.MobileSocket`, the path for the initial HTTP request is:
[host]:[port]/mobile/websocket?vsn=2.0.0
Passing `&vsn=2.0.0` specifies `Phoenix.Socket.V2.JSONSerializer`, which is built into Phoenix, and which expects and returns messages in the form of lists.
You also need to include [the standard header fields for upgrading an HTTP request to a WebSocket connection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) or use an HTTP library that handles this for you; in Elixir, [mint_web_socket](https://hex.pm/packages/mint_web_socket) is an example.
Other parameters or headers may be expected or required by the specific `connect/3` function in the application's socket module (in the example above, `MyAppWeb.MobileSocket.connect/3`).
## Message Format
The message format is determined by the serializer configured for the application.
For these examples, `Phoenix.Socket.V2.JSONSerializer` is assumed.
The general format for messages a client sends to a Phoenix Channel is as follows:
```
[join_reference, message_reference, topic_name, event_name, payload]
```
- The `join_reference` is also chosen by the client and should also be a unique value. It only needs to be sent for a `"phx_join"` event; for other messages it can be `null`. It is used as a message reference for `push` messages from the server, meaning those that are not replies to a specific client message. For example, imagine something like "a new user just joined the chat room".
- The `message_reference` is chosen by the client and should be a unique value. The server includes it in its reply so that the client knows which message the reply is for.
- The `topic_name` must be a known topic for the socket endpoint, and a client must join that topic before sending any messages on it.
- The `event_name` must match the first argument of a `handle_in` function on the server channel module.
- The `payload` should be a map and is passed as the second argument to that `handle_in` function.
There are three events that are understood by every Phoenix application.
First, `phx_join` is used join a channel. For example, to join the `miami:weather` channel:
```json
["0", "0", "miami:weather", "phx_join", {"some": "param"}]
```
Second, `phx_leave` is used to leave a channel. For example, to leave the `miami:weather` channel:
```json
[null, "1", "miami:weather", "phx_leave", {}]
```
Third, `heartbeat` is used to maintain the WebSocket connection. For example:
```json
[null, "2", "phoenix", "heartbeat", {}]
```
The `heartbeat` message is only needed when no other messages are being sent and prevents Phoenix from closing the connection; the exact `:timeout` is configured in the application's `Endpoint` module.
Other allowed messages depend on the Phoenix application.
For example, if the Channel serving the `miami:weather` can handle a `report_emergency` event:
```elixir
def handle_in("report_emergency", payload, socket) do
MyApp.Emergencies.report(payload) # or whatever
{:reply, :ok, socket}
end
```
...a client could send:
```json
[null, "3", "miami:weather", "report_emergency", {"category": "sharknado"}]
```
================================================
FILE: guides/introduction/community.md
================================================
# Community
The Elixir and Phoenix communities are friendly and welcoming. All questions and comments are valuable, so please come join the discussion!
There are a number of places to connect with community members at all experience levels.
* We're on Libera IRC in the [\#elixir](https://web.libera.chat/?channels=#elixir) channel.
* Feel free to join and check out the #phoenix channel on [Discord](https://discord.gg/elixir).
* Read about [bug reports](https://github.com/phoenixframework/phoenix/blob/main/CONTRIBUTING.md#bug-reports) or open an issue in the Phoenix [issue tracker](https://github.com/phoenixframework/phoenix/issues).
* Ask or answer questions about Phoenix on [Elixir Forum](https://elixirforum.com/c/phoenix-forum) or [Stack Overflow](https://stackoverflow.com/questions/tagged/phoenix-framework).
* Follow the Phoenix Framework on [Twitter](https://twitter.com/elixirphoenix).
## Security
For information about security patches and how to report a vulnerability in Phoenix, see the [security policy.](https://github.com/phoenixframework/phoenix/security)
To learn about how to secure a web application written with Phoenix, see the [security documentation page.](/guides/security.md)
The Erlang Ecosystem Foundation also publishes in-depth documents which are relevant for Erlang, Elixir, and Phoenix developers. These include:
* [Web Application Security Best Practices for BEAM languages](https://security.erlef.org/web_app_security_best_practices_beam/)
* [Secure Coding and Deployment Hardening Guidelines](https://security.erlef.org/secure_coding_and_deployment_hardening/)
## Books
* [Programming Phoenix LiveView - Interactive Elixir Web Programming Without Writing Any JavaScript - 2023 (by Bruce Tate and Sophie DeBenedetto)](https://pragprog.com/titles/liveview/programming-phoenix-liveview/)
* [Phoenix Tutorial (Phoenix 1.6)](https://thephoenixtutorial.org/) - [Free to read online](https://thephoenixtutorial.org/book)
* [Real-Time Phoenix - Build Highly Scalable Systems with Channels (by Stephen Bussey - 2020)](https://pragprog.com/titles/sbsockets/real-time-phoenix/)
* [Programming Phoenix 1.4 (by Bruce Tate, Chris McCord, and José Valim - 2019)](https://pragprog.com/titles/phoenix14/programming-phoenix-1-4/)
* [Phoenix in Action (by Geoffrey Lessel - 2019)](https://manning.com/books/phoenix-in-action)
* [Phoenix Inside Out - Book Series (by Shankar Dhanasekaran - 2017)](https://shankardevy.com/phoenix-book/). First book of the series Mastering Phoenix Framework is [free to read online](https://shankardevy.com/phoenix-inside-out-mpf/)
* [Functional Web Development with Elixir, OTP, and Phoenix Rethink the Modern Web App (by Lance Halvorsen - 2017)](https://pragprog.com/titles/lhelph/functional-web-development-with-elixir-otp-and-phoenix/)
## Screencasts/Courses
* [Full-Stack Phoenix Course (by The Pragmatic Studio - 2025)](https://pragmaticstudio.com/courses/phoenix)
* [Free Bootcamp: Fullstack Elixir and Phoenix (by TechSchool - 2024)](https://techschool.dev/en/bootcamps/fullstack-elixir-and-phoenix)
* [Learn Phoenix LiveView (by George Arrowsmith - 2024)](https://phoenixliveview.com)
* [Phoenix LiveView Course (by The Pragmatic Studio - 2023)](https://pragmaticstudio.com/courses/phoenix-liveview)
* [Build It With Phoenix video course (by Geoffrey Lessel - 2023)](https://builditwithphoenix.com)
* [Free Crash Course: Phoenix LiveView (by Productive Programmer - 2023)](https://www.productiveprogrammer.com/learn-phoenix-liveview-free)
* [Phoenix on Rails: Elixir and Phoenix for Ruby on Rails developers (by George Arrowsmith - 2023)](https://phoenixonrails.com)
* [Groxio LiveView: Self Study Program (by Bruce Tate - 2020)](https://grox.io/language/liveview/course)
* [Alchemist Camp: Learn Elixir and Phoenix by building (2018-2022)](https://alchemist.camp/episodes)
* [The Complete Elixir and Phoenix Bootcamp Master Functional Programming Techniques with Elixir and Phoenix while Learning to Build Compelling Web Applications (by Stephen Grider - 2017)](https://www.udemy.com/the-complete-elixir-and-phoenix-bootcamp-and-tutorial/)
* [Discover Elixir & Phoenix (by Tristan Edwards - 2017)](https://www.ludu.co/course/discover-elixir-phoenix)
* [Phoenix Framework Tutorial (by Tensor Programming - 2017)](https://www.youtube.com/watch?v=irDC1nWKhZ8&index=6&list=PLJbE2Yu2zumAgKjSPyFtvYjP5LqgzafQq)
* [Getting Started with Phoenix (by Pluralsight - 2017)](https://www.pluralsight.com/courses/phoenix-getting-started)
* [LearnPhoenix.tv: Learn how to Build Fast, Dependable Web Apps with Phoenix (2017)](https://www.learnphoenix.tv/)
* [LearnPhoenix.io: Build Scalable, Real-Time Apps with Phoenix, React, and React Native (2016)](https://www.learnphoenix.io/)
================================================
FILE: guides/introduction/installation.md
================================================
# Installation
In order to build a Phoenix application, we will need a few dependencies installed in our Operating System:
* the Erlang VM and the Elixir programming language
* a database - Phoenix recommends PostgreSQL, but you can pick others or not use a database at all
* and other optional packages.
Please take a look at this list and make sure to install anything necessary for your system. Having dependencies installed in advance can prevent frustrating problems later on.
If you just want to get started quickly, the [Up and Running](up_and_running.md) page includes a link to Phoenix Express, which will get Erlang, Elixir and Phoenix installed and running in seconds.
## Elixir 1.15 or later
Phoenix is written in Elixir, and our application code will also be written in Elixir. We won't get far in a Phoenix app without it! The Elixir site maintains a great [Installation Page](https://elixir-lang.org/install.html) to help.
## Erlang 24 or later
Elixir code compiles to Erlang byte code to run on the Erlang virtual machine. Without Erlang, Elixir code has no virtual machine to run on, so we need to install Erlang as well.
When we install Elixir using instructions from the Elixir [Installation Page](https://elixir-lang.org/install.html), we will usually get Erlang too. If Erlang was not installed along with Elixir, please see the [Erlang Instructions](https://elixir-lang.org/install.html#installing-erlang) section of the Elixir Installation Page for instructions.
## Phoenix
To check that we are on Elixir 1.15 and Erlang 24 or later, run:
```console
elixir -v
Erlang/OTP 24 [erts-12.0] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Elixir 1.15.0
```
Once we have Elixir and Erlang, we are ready to install the Phoenix application generator:
```console
$ mix archive.install hex phx_new
```
The `phx.new` generator is now available to generate new applications in the next guide, called [Up and Running](up_and_running.html). The flags mentioned below are command line options to the generator; see all available options by calling `mix help phx.new`.
## PostgreSQL
PostgreSQL is a relational database server. Phoenix configures applications to use it by default, but we can switch to MySQL, MSSQL, or SQLite3 by passing the `--database` flag when creating a new application.
In order to talk to databases, Phoenix applications use another Elixir package, called [Ecto](https://github.com/elixir-ecto/ecto). If you don't plan to use databases in your application, you can pass the `--no-ecto` flag.
However, if you are just getting started with Phoenix, we recommend you to install PostgreSQL and make sure it is running. The PostgreSQL wiki has [installation guides](https://wiki.postgresql.org/wiki/Detailed_installation_guides) for a number of different systems.
## inotify-tools (for Linux users)
Phoenix provides a very handy feature called Live Reloading. As you change your views or your assets, it automatically reloads the page in the browser. In order for this functionality to work, you need a filesystem watcher.
macOS and Windows users already have a filesystem watcher, but Linux users must install inotify-tools. Please consult the [inotify-tools wiki](https://github.com/rvoicilas/inotify-tools/wiki) for distribution-specific installation instructions.
## Summary
At the end of this section, you must have installed Elixir, Hex, Phoenix, and PostgreSQL. Now that we have everything installed, let's create our first Phoenix application and get [up and running](up_and_running.html).
================================================
FILE: guides/introduction/overview.md
================================================
# Overview
Phoenix is a web development framework written in Elixir which implements the server-side Model View Controller (MVC) pattern. Many of its components and concepts will seem familiar to those of us with experience in other web frameworks like Ruby on Rails or Python's Django.
Phoenix provides the best of both worlds - high developer productivity _and_ high application performance. It also has some interesting new twists like channels for implementing realtime features and pre-compiled templates for blazing speed.
If you are already familiar with Elixir, great! If not, there are a number of places to learn. The [Elixir guides](https://hexdocs.pm/elixir/introduction.html) and the [Elixir learning resources page](https://elixir-lang.org/learning.html) are two great places to start.
The guides that you are currently looking at provide an overview of all parts that make Phoenix. Here is a rundown of what they provide:
* Introduction - the guides you are currently reading. They will cover how to get your first application up and running
* Guides - in-depth guides covering the main components in Phoenix and Phoenix applications
* Data modelling - building the initial features of an e-commerce application to learn about more data modelling with Phoenix
* Authn and Authz - learn how to use the tools Phoenix provides for authentication and authorization
* Real-time components - in-depth guides covering Phoenix's built-in real-time components
* Testing - in-depth guides about testing
* Deployment - in-depth guides about deployment
* How-to's - a collection of articles on how to achieve certain things with Phoenix
If you would prefer to read these guides as an EPUB, [click here!](Phoenix.epub)
Note, these guides are not a step-by-step introduction to Phoenix. If you want a more structured approach to learning the framework, we have a large community and many books, courses, and screencasts available. See [our community page](community.html) for a complete list.
[Let's get Phoenix installed](installation.html).
================================================
FILE: guides/introduction/packages_glossary.md
================================================
# Packages Glossary
By default, Phoenix applications depend on several packages with different purposes.
This page is a quick reference of the different packages you may work with as a Phoenix
developer.
The main packages are:
* [Ecto](https://hexdocs.pm/ecto) - a language integrated query and
database wrapper
* [Phoenix](https://hexdocs.pm/phoenix) - the Phoenix web framework
(these docs)
* [Phoenix LiveView](https://hexdocs.pm/phoenix_live_view) - build rich,
real-time user experiences with server-rendered HTML. The LiveView
project also defines [`Phoenix.Component`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html) and
[the HEEx template engine](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2),
used for rendering HTML content in both regular and real-time applications
* [Plug](https://hexdocs.pm/plug) - specification and conveniences for
building composable modules web applications. This is the package
responsible for the connection abstraction and the regular request-
response life-cycle
You will also work with the following:
* [ExUnit](https://hexdocs.pm/ex_unit) - Elixir's built-in test framework
* [Gettext](https://hexdocs.pm/gettext) - internationalization and
localization through [`gettext`](https://www.gnu.org/software/gettext/)
* [Swoosh](https://hexdocs.pm/swoosh) - a library for composing,
delivering and testing emails, also used by `mix phx.gen.auth`
When peeking under the covers, you will find these libraries play
an important role in Phoenix applications:
* [Phoenix HTML](https://hexdocs.pm/phoenix_html) - building blocks
for working with HTML and forms safely
* [Phoenix Ecto](https://hex.pm/packages/phoenix_ecto) - plugs and
protocol implementations for using phoenix with ecto
* [Phoenix PubSub](https://hexdocs.pm/phoenix_pubsub) - a distributed
pub/sub system with presence support
When it comes to instrumentation and monitoring, check out:
* [Phoenix LiveDashboard](https://hexdocs.pm/phoenix_live_dashboard) -
real-time performance monitoring and debugging tools for Phoenix
developers
* [Telemetry Metrics](https://hexdocs.pm/telemetry_metrics) - common
interface for defining metrics based on Telemetry events
================================================
FILE: guides/introduction/up_and_running.md
================================================
# Up and Running
There are two mechanisms to start a new Phoenix application: the express option, supported on some OSes, and via `mix phx.new`. Let's check it out.
## Phoenix Express
A single command will get you up and running in seconds:
For macOS/Ubuntu:
```bash
$ curl https://new.phoenixframework.org/myapp | sh
```
For Windows PowerShell:
```bash
curl.exe -fsSO https://new.phoenixframework.org/myapp.bat; .\myapp.bat
```
The above will install Erlang, Elixir, and Phoenix, and generate a fresh Phoenix application. It will also automatically pick one of PostgreSQL or MySQL as the database, and fallback to SQLite if none of them are available. Once the command above completes, it will open up a Phoenix application, with the steps necessary to complete your installation. Note your Phoenix application name is taken from the path.
If your operating system is not supported, or the command above fails, don't fret! You can still start your Phoenix application using `mix phx.new`.
## Via `mix phx.new`
In order to create a new Phoenix application, you will need to install Erlang, Elixir, and Phoenix. See the [Installation Guide](installation.html) for more information. If you share your application with someone, they will also need to follow the Installation Guide steps to set it all up.
Once you are ready, you can run `mix phx.new` from any directory in order to bootstrap our Phoenix application. Phoenix will accept either an absolute or relative path for the directory of our new project. Assuming that the name of our application is `hello`, let's run the following command:
```console
$ mix phx.new hello
```
> By default, `mix phx.new` includes a number of optional dependencies, for example:
>
> - [Ecto](ecto.html) for communicating with a data store, such as PostgreSQL, MySQL, and others. You can skip this with `--no-ecto`.
>
> - [Phoenix.HTML](https://hexdocs.pm/phoenix_html/Phoenix.HTML.html), [TailwindCSS](https://tailwindcss.com), and [Esbuild](https://esbuild.github.io) for HTML applications. You can skip them with the `--no-html` and `--no-assets` flags.
>
> - [Phoenix.LiveView](https://hexdocs.pm/phoenix_live_view/) for building realtime and interactive web applications. You can skip this with `--no-live`.
>
> Run `mix help phx.new` to learn all options.
```console
mix phx.new hello
* creating hello/config/config.exs
* creating hello/config/dev.exs
* creating hello/config/prod.exs
...
Fetch and install dependencies? [Yn]
```
Phoenix generates the directory structure and all the files we will need for our application.
> Phoenix promotes the usage of git as version control software: among the generated files we find a `.gitignore`. We can `git init` our repository, and immediately add and commit all that hasn't been marked ignored.
When it's done, it will ask us if we want it to install our dependencies for us. Let's say yes to that.
```console
Fetch and install dependencies? [Yn] Y
* running mix deps.get
* running mix assets.setup
* running mix deps.compile
We are almost there! The following steps are missing:
$ cd hello
Then configure your database in config/dev.exs and run:
$ mix ecto.create
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
```
Once our dependencies are installed, the task will prompt us to change into our project directory and start our application.
Phoenix assumes that our PostgreSQL database will have a `postgres` user account with the correct permissions and a password of "postgres". Let's give it a try.
First, we'll `cd` into the `hello/` directory we've just created:
```console
$ cd hello
```
Now we'll create our database:
```console
$ mix ecto.create
Compiling 13 files (.ex)
Generated hello app
The database for Hello.Repo has been created
```
In case the database could not be created, see [our Ecto section on Mix tasks](ecto.html#mix-tasks) or run `mix help ecto.create`.
And finally, we'll start the Phoenix server:
```console
$ mix phx.server
[info] Running HelloWeb.Endpoint with Bandit 1.5.7 at 127.0.0.1:4000 (http)
[info] Access HelloWeb.Endpoint at http://localhost:4000
[watch] build finished, watching for changes...
...
```
If we choose not to have Phoenix install our dependencies when we generate a new application, the `mix phx.new` task will prompt us to take the necessary steps when we do want to install them.
```console
Fetch and install dependencies? [Yn] n
We are almost there! The following steps are missing:
$ cd hello
$ mix deps.get
Then configure your database in config/dev.exs and run:
$ mix ecto.create
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
```
By default, Phoenix accepts requests on port 4000. If we point our favorite web browser at [http://localhost:4000](http://localhost:4000), we should see the Phoenix Framework welcome page.
If your screen looks like the image above, congratulations! You now have a working Phoenix application. In case you can't see the page above, try accessing it via [http://127.0.0.1:4000](http://127.0.0.1:4000) and later make sure your OS has defined "localhost" as "127.0.0.1".
To stop it, we hit `ctrl-c` twice.
Now you are ready to explore the world provided by Phoenix! See [our community page](community.html) for books, screencasts, courses, and more.
Alternatively, you can continue reading these guides to have a quick introduction into all the parts that make your Phoenix application. If that's the case, you can read the guides in any order or start with our guide that explains the [Phoenix directory structure](directory_structure.html).
================================================
FILE: guides/json_and_apis.md
================================================
# JSON and APIs
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
> **Requirement**: This guide expects that you have gone through the [Controllers guide](controllers.html).
You can also use the Phoenix Framework to build [Web APIs](https://en.wikipedia.org/wiki/Web_API). By default Phoenix supports JSON but you can bring any other rendering format you desire.
## The JSON API
For this guide let's create a simple JSON API to store our favourite links, that will support all the CRUD (Create, Read, Update, Delete) operations out of the box.
For this guide, we will use Phoenix generators to scaffold our API infrastructure:
```console
mix phx.gen.json Urls Url urls link:string title:string
* creating lib/hello_web/controllers/url_controller.ex
* creating lib/hello_web/controllers/url_json.ex
* creating lib/hello_web/controllers/changeset_json.ex
* creating test/hello_web/controllers/url_controller_test.exs
* creating lib/hello_web/controllers/fallback_controller.ex
* creating lib/hello/urls/url.ex
* creating priv/repo/migrations/20221129120234_create_urls.exs
* creating lib/hello/urls.ex
* injecting lib/hello/urls.ex
* creating test/hello/urls_test.exs
* injecting test/hello/urls_test.exs
* creating test/support/fixtures/urls_fixtures.ex
* injecting test/support/fixtures/urls_fixtures.ex
```
We will break those files into four categories:
* Files in `lib/hello_web` responsible for effectively rendering JSON
* Files in `lib/hello` responsible for defining our context and logic to persist links to the database
* Files in `priv/repo/migrations` responsible for updating our database
* Files in `test` to test our controllers and contexts
In this guide, we will explore only the first category of files. To learn more about how Phoenix stores and manage data, check out [the Ecto guide](ecto.md) and [the Contexts guide](contexts.md) for more information. We also have a whole section dedicated to testing.
At the end, the generator asks us to add the `/url` resource to our `:api` scope in `lib/hello_web/router.ex`:
```elixir
scope "/api", HelloWeb do
pipe_through :api
resources "/urls", UrlController, except: [:new, :edit]
end
```
The API scope uses the `:api` pipeline, which will run specific steps such as ensuring the client can handle JSON responses.
Then we need to update our repository by running migrations:
```console
$ mix ecto.migrate
```
### Trying out the JSON API
Before we go ahead and change those files, let's take a look at how our API behaves from the command line.
First, we need to start the server:
```console
$ mix phx.server
```
Next, let's make a smoke test to check our API is working with:
```console
$ curl -i http://localhost:4000/api/urls
```
If everything went as planned we should get a `200` response:
```console
HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 11
content-type: application/json; charset=utf-8
date: Fri, 06 May 2022 21:22:42 GMT
server: Cowboy
x-request-id: Fuyg-wMl4S-hAfsAAAUk
{"data":[]}
```
We didn't get any data because we haven't populated the database with any yet. So let's add some links:
```console
$ curl -iX POST http://localhost:4000/api/urls \
-H 'Content-Type: application/json' \
-d '{"url": {"link":"https://phoenixframework.org", "title":"Phoenix Framework"}}'
$ curl -iX POST http://localhost:4000/api/urls \
-H 'Content-Type: application/json' \
-d '{"url": {"link":"https://elixir-lang.org", "title":"Elixir"}}'
```
Now we can retrieve all links:
```console
$ curl -i http://localhost:4000/api/urls
```
Or we can just retrieve a link by its `id`:
```console
$ curl -i http://localhost:4000/api/urls/1
```
Next, we can update a link with:
```console
$ curl -iX PUT http://localhost:4000/api/urls/2 \
-H 'Content-Type: application/json' \
-d '{"url": {"title":"Elixir Programming Language"}}'
```
The response should be a `200` with the updated link in the body.
Finally, we need to try out the removal of a link:
```console
$ curl -iX DELETE http://localhost:4000/api/urls/2 \
-H 'Content-Type: application/json'
```
A `204` response should be returned to indicate the successful removal of the link.
## Rendering JSON
To understand how to render JSON, let's start with the `index` action from `UrlController` defined at `lib/hello_web/controllers/url_controller.ex`:
```elixir
def index(conn, _params) do
urls = Urls.list_urls()
render(conn, :index, urls: urls)
end
```
As we can see, this is not any different from how Phoenix renders HTML templates. We call `render/3`, passing the connection, the template we want our views to render (`:index`), and the data we want to make available to our views.
Phoenix typically uses one view per rendering format. When rendering HTML, we would use `UrlHTML`. Now that we are rendering JSON, we will find a `UrlJSON` view collocated with the template at `lib/hello_web/controllers/url_json.ex`. Let's open it up:
```elixir
defmodule HelloWeb.UrlJSON do
alias Hello.Urls.Url
@doc """
Renders a list of urls.
"""
def index(%{urls: urls}) do
%{data: for(url <- urls, do: data(url))}
end
@doc """
Renders a single url.
"""
def show(%{url: url}) do
%{data: data(url)}
end
defp data(%Url{} = url) do
%{
id: url.id,
link: url.link,
title: url.title
}
end
end
```
This view is very simple. The `index` function receives all URLs, and converts them into a list of maps. Those maps are placed inside the data key at the root, exactly as we saw when interfacing with our application from `cURL`. In other words, our JSON view converts our complex data into simple Elixir data-structures. Once our view layer returns, Phoenix uses the `Jason` library to encode JSON and send the response to the client.
If you explore the remaining controller, you will learn the `show` action is similar to the `index` one. For `create`, `update`, and `delete` actions, Phoenix uses one other important feature, called "Action fallback".
## Reading request data
As we've seen in [Request life-cycle](request_lifecycle.html), all controller actions take two arguments, `conn` and `params`. Plug automatically parses path parameters, query parameters and the request body into the `params` argument.
### Minimal example
Let's consider this minimal setup, with three API routes (`lib/hello_web/router.ex`):
```elixir
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :api do
plug :accepts, ["json"]
end
scope "/api", HelloWeb do
pipe_through :api
get "/", HelloController, :show
get "/:name", HelloController, :show
post "/", HelloController, :show
end
end
```
A controller with a single action (`lib/hello_web/controllers/hello_controller.ex`):
```elixir
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def show(conn, params) do
render(conn, :greet, params)
end
end
```
and a JSON view (`lib/hello_web/controllers/hello_json.ex`):
```elixir
defmodule HelloWeb.HelloJSON do
def greet(%{"name" => name}) do
%{greeting: "Hello #{name}!"}
end
end
```
This simple setup can process all of the following requests:
```console
$ curl http://localhost:4000/api/world
$ curl http://localhost:4000/api?name=world
$ curl -iX POST http://localhost:4000/api \
-H 'Content-Type: application/json' \
-d '{"name": "world"}'
```
In the exact same way: `Hello world!`
### Request data is merged
Since all data is merged under a single map, providing more than one source of data (such as both a path parameter and a JSON body, or a query parameter and a form), will result in **fields with the same name overriding each other.**
The priority is as follows: `Path Parameters > Body (any kind) > Query Parameters`
Following our last example, lets add another POST request handler which accepts a path parameter:
```elixir
scope "/api", HelloWeb do
pipe_through :api
get "/", HelloController, :show
get "/:name", HelloController, :show
post "/", HelloController, :show
post "/:name", HelloController, :show # New route
# ^^^^^ New parameter
end
```
Now consider the following requests:
```console
$ curl -iX POST http://localhost:4000/api/name1?name=name3 \
-H 'Content-Type: application/json' \
-d '{"name": "name2"}'
$ curl -iX POST http://localhost:4000/api?name=name3 \
-H 'Content-Type: application/json' \
-d '{"name": "name2"}'
$ curl -iX POST http://localhost:4000/api?name=name3
```
They would return, respectively, `Hello name1!` (Path parameter), `Hello name2!` (Request body) and `Hello name3!` (Query parameter).
You can access those parameters individually if desired via `conn.path_params`, `conn.body_params`, and `conn.query_params` respectively.
## Action fallback
Action fallback allows us to centralize error handling code in plugs, which are called when a controller action fails to return a [`%Plug.Conn{}`](`t:Plug.Conn.t/0`) struct. These plugs receive both the `conn` which was originally passed to the controller action along with the return value of the action.
Let's say we have a `show` action which uses [`with`](`with/1`) to fetch a blog post and then authorize the current user to view that blog post. In this example we might expect `fetch_post/1` to return `{:error, :not_found}` if the post is not found and `authorize_user/3` might return `{:error, :unauthorized}` if the user is unauthorized. We could use our `ErrorHTML` and `ErrorJSON` views which are generated by Phoenix for every new application to handle these error paths accordingly:
```elixir
defmodule HelloWeb.MyController do
use Phoenix.Controller
def show(conn, %{"id" => id}, current_user) do
with {:ok, post} <- fetch_post(id),
:ok <- authorize_user(current_user, :view, post) do
render(conn, :show, post: post)
else
{:error, :not_found} ->
conn
|> put_status(:not_found)
|> put_view(html: HelloWeb.ErrorHTML, json: HelloWeb.ErrorJSON)
|> render(:"404")
{:error, :unauthorized} ->
conn
|> put_status(403)
|> put_view(html: HelloWeb.ErrorHTML, json: HelloWeb.ErrorJSON)
|> render(:"403")
end
end
end
```
Now imagine you may need to implement similar logic for every controller and action handled by your API. This would result in a lot of repetition.
Instead we can define a module plug which knows how to handle these error cases specifically. Since controllers are module plugs, let's define our plug as a controller:
```elixir
defmodule HelloWeb.MyFallbackController do
use Phoenix.Controller
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> put_view(json: HelloWeb.ErrorJSON)
|> render(:"404")
end
def call(conn, {:error, :unauthorized}) do
conn
|> put_status(403)
|> put_view(json: HelloWeb.ErrorJSON)
|> render(:"403")
end
end
```
Then we can reference our new controller as the `action_fallback` and simply remove the `else` block from our `with`:
```elixir
defmodule HelloWeb.MyController do
use Phoenix.Controller
action_fallback HelloWeb.MyFallbackController
def show(conn, %{"id" => id}, current_user) do
with {:ok, post} <- fetch_post(id),
:ok <- authorize_user(current_user, :view, post) do
render(conn, :show, post: post)
end
end
end
```
Whenever the `with` conditions do not match, `HelloWeb.MyFallbackController` will receive the original `conn` as well as the result of the action and respond accordingly.
## FallbackController and ChangesetJSON
With this knowledge in hand, we can explore the `FallbackController` (`lib/hello_web/controllers/fallback_controller.ex`) generated by `mix phx.gen.json`. In particular, it handles one clause (the other is generated as an example):
```elixir
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(json: HelloWeb.ChangesetJSON)
|> render(:error, changeset: changeset)
end
```
The goal of this clause is to handle the `{:error, changeset}` return types from the `HelloWeb.Urls` context and render them into rendered errors via the `ChangesetJSON` view. Let's open up `lib/hello_web/controllers/changeset_json.ex` to learn more:
```elixir
defmodule HelloWeb.ChangesetJSON do
@doc """
Renders changeset errors.
"""
def error(%{changeset: changeset}) do
# When encoded, the changeset returns its errors
# as a JSON object. So we just pass it forward.
%{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)}
end
end
```
As we can see, it will convert the errors into a data structure, which will be rendered as JSON. The changeset is a data structure responsible for casting and validating data. For our example, it is defined in `Hello.Urls.Url.changeset/1`. Let's open up `lib/hello/urls/url.ex` and see its definition:
```elixir
@doc false
def changeset(url, attrs) do
url
|> cast(attrs, [:link, :title])
|> validate_required([:link, :title])
end
```
As you can see, the changeset requires both link and title to be given. This means we can try posting a url with no link and title and see how our API responds:
```console
$ curl -iX POST http://localhost:4000/api/urls \
-H 'Content-Type: application/json' \
-d '{"url": {}}'
{"errors": {"link": ["can't be blank"], "title": ["can't be blank"]}}
```
Feel free to modify the `changeset` function and see how your API behaves.
## API-only applications
In case you want to generate a Phoenix application exclusively for APIs, you can pass
several options when invoking `mix phx.new`. Let's check which `--no-*` flags we need
to use to not generate the scaffolding that isn't necessary on our Phoenix application
for the REST API.
From your terminal run:
```console
$ mix help phx.new
```
The output should contain the following:
```text
• --no-assets - equivalent to --no-esbuild and --no-tailwind
• --no-dashboard - do not include Phoenix.LiveDashboard
• --no-ecto - do not generate Ecto files
• --no-esbuild - do not include esbuild dependencies and
assets. We do not recommend setting this option, unless for API
only applications, as doing so requires you to manually add and
track JavaScript dependencies
• --no-gettext - do not generate gettext files
• --no-html - do not generate HTML views
• --no-live - comment out LiveView socket setup in your Endpoint
and assets/js/app.js. Automatically disabled if --no-html is given
• --no-mailer - do not generate Swoosh mailer files
• --no-tailwind - do not include tailwind dependencies and
assets. The generated markup will still include Tailwind CSS
classes, those are left-in as reference for the subsequent
styling of your layout and components
```
The `--no-html` is the obvious one we want to use when creating any Phoenix application for an API in order to leave out all the unnecessary HTML scaffolding. You may also pass `--no-assets`, if you don't want any of the asset management bit, `--no-gettext` if you don't support internationalization, and so on.
So, in order to generate a simple API called **Hello**, without any frontend, database connection, internationalization or mailing service, you can provide the following flags:
```
$ mix phx.new hello --no-assets --no-dashboard --no-ecto --no-gettext --no-html --no-mailer
```
This still includes the Telemetry, PubSub and DNS Cluster modules by default, which you can remove manually if necessary.
Also bear in mind that nothing stops you to have a backend that supports simultaneously the REST API and a Web App (HTML, assets, internationalization and sockets).
================================================
FILE: guides/live_view.md
================================================
# LiveView
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
> **Requirement**: This guide expects that you have gone through the [request life-cycle guide](request_lifecycle.html).
We've already seen how the typical request lifecycle in Phoenix works: a request is matched in the router, a controller handles the request and turns to a view to return a response in the correct format. But what if we want to build interactive pages? In a typical server rendered application, changing the content of the page either needs a form submission rendering the new page, or moving application logic to the client (JavaScript frameworks like jQuery, React, Vue, etc.) and building an API interface for the client to talk to.
Phoenix LiveView offers a different approach, keeping all the state on the server while providing rich, real-time user experiences with server-rendered HTML. It's an alternative to client-side JavaScript frameworks that allows you to build dynamic, interactive applications with minimal JavaScript code on the client.
## What is a LiveView?
LiveViews are processes that receive events, update their state, and render updates to a page as diffs.
The LiveView programming model is declarative: instead of saying "once event X happens, change Y on the page", events in LiveView are regular messages which may cause changes to the state. Once the state changes, the LiveView will re-render the relevant parts of its HTML template and push it to the browser, which updates the page in the most efficient manner.
LiveView state is nothing more than functional and immutable Elixir data structures. The events are either internal application messages (usually emitted by `Phoenix.PubSub`) or sent by the client/browser.
Every LiveView is first rendered statically as part of a regular HTTP request, which provides quick times for "First Meaningful Paint", in addition to helping search and indexing engines. A persistent connection is then established between the client and server to exchange events and changes to the page. This allows LiveView applications to react faster to user events as there is less work to be done and less data to be sent compared to stateless requests that have to authenticate, decode, load, and encode data on every request. You can think of LiveView as "diffs over the wire".
## LiveView vs Controller + View
While Phoenix controllers and LiveViews serve similar purposes in handling user interactions, they operate very differently:
### Controller + View
- Controllers handle each HTTP request-response pair as separate transactions
- Each page load or form submission requires a full request/response cycle
- Controllers are stateless, with data stored externally (database, session)
- Views are separate modules that render templates with the data from controllers
- Page updates and dynamic interactions require either full page reloads or custom client-side JavaScript code
### LiveView approach
- Initial page load uses the regular request lifecycle, but then establishes a bidirectional connection using [Phoenix Channels](channels.md)
- A LiveView process maintains state throughout user interaction
- State changes automatically trigger re-renders of only the changed parts of the page
- Events flow through the persistent connection instead of separate HTTP requests
- Minimal JavaScript is required for interactive features
LiveViews combine the concerns of controllers and views into a more unified model.
## Basic example
LiveView is included by default in new Phoenix applications. Let's see a simple example:
```elixir
defmodule MyAppWeb.ThermostatLive do
use MyAppWeb, :live_view
def render(assigns) do
~H"""
Current temperature: {@temperature}°F
"""
end
def mount(_params, _session, socket) do
temperature = 70 # Let's assume a fixed temperature for now
{:ok, assign(socket, :temperature, temperature)}
end
def handle_event("inc_temperature", _params, socket) do
{:noreply, update(socket, :temperature, &(&1 + 1))}
end
end
```
This LiveView demonstrates the core lifecycle:
1. The `mount/3` callback initializes state when the LiveView starts
2. The `render/1` function defines what is displayed using [HEEx templates](components.md)
3. The `handle_event/3` callback responds to events from the client
To wire this up in your router:
```elixir
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
...
end
scope "/", MyAppWeb do
pipe_through :browser
...
live "/thermostat", ThermostatLive
end
end
```
Once the LiveView is rendered, a regular HTML response is sent. In your
app.js file, you should find the following:
```javascript
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
liveSocket.connect()
```
Now the JavaScript client will connect over WebSockets and `mount/3` will be invoked
inside a spawned LiveView process.
## Key concepts
### Socket and state
The LiveView socket is the fundamental data structure that holds all state in a LiveView. It's an immutable structure containing "assigns" - the data available to your templates. While controllers have `conn`, LiveViews have `socket`.
Changes to the socket (via `assign/3` or `update/3`) trigger re-renders. All state is maintained on the server, with only the diffs sent to the client, minimizing network traffic.
### LiveView lifecycle
LiveViews have several important lifecycle stages:
- [`mount`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:mount/3) - initializes the LiveView with parameters, session data, and socket
- [`handle_params`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_params/3) - responds to URL changes and updates LiveView state accordingly
- [`handle_event`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_event/3) - responds to user interactions coming from the client
- [`handle_info`](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#c:handle_info/2) - responds to regular process messages
### DOM Bindings
LiveView provides DOM bindings for convenient client-server interaction:
```html
```
These bindings automatically send events to the server when the specified browser events occur, which are then handled in `handle_event/3`.
## Getting Started
Phoenix includes code generators for LiveView. Try:
```
$ mix phx.gen.live Blog Post posts title:string body:text
```
This generates a complete LiveView CRUD implementation, similar to `mix phx.gen.html`.
To learn more about LiveView, please refer to the [Phoenix LiveView documentation](https://hexdocs.pm/phoenix_live_view).
================================================
FILE: guides/plug.md
================================================
# Plug
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
> **Requirement**: This guide expects that you have gone through the [Request life-cycle guide](request_lifecycle.html).
Plug lives at the heart of Phoenix's HTTP layer, and Phoenix puts Plug front and center. We interact with plugs at every step of the request life-cycle, and the core Phoenix components like endpoints, routers, and controllers are all just plugs internally. Let's jump in and find out just what makes Plug so special.
[Plug](https://github.com/elixir-lang/plug) is a specification for composable modules in between web applications. It is also an abstraction layer for connection adapters of different web servers. The basic idea of Plug is to unify the concept of a "connection" that we operate on. This differs from other HTTP middleware layers such as Rack, where the request and response are separated in the middleware stack.
At the simplest level, the Plug specification comes in two flavors: *function plugs* and *module plugs*.
## Function plugs
In order to act as a plug, a function needs to:
1. accept a connection struct (`%Plug.Conn{}`) as its first argument, and connection options as its second one;
2. return a connection struct.
Any function that meets these two criteria will do. Here's an example.
```elixir
def introspect(conn, _opts) do
IO.puts """
Verb: #{inspect(conn.method)}
Host: #{inspect(conn.host)}
Headers: #{inspect(conn.req_headers)}
"""
conn
end
```
This function does the following:
1. It receives a connection and options (that we do not use)
2. It prints some connection information to the terminal
3. It returns the connection
Pretty simple, right? Let's see this function in action by adding it to our endpoint in `lib/hello_web/endpoint.ex`. We can plug it anywhere, so let's do it by inserting `plug :introspect` right before we delegate the request to the router:
```elixir
defmodule HelloWeb.Endpoint do
...
plug :introspect
plug HelloWeb.Router
def introspect(conn, _opts) do
IO.puts """
Verb: #{inspect(conn.method)}
Host: #{inspect(conn.host)}
Headers: #{inspect(conn.req_headers)}
"""
conn
end
end
```
Function plugs are plugged by passing the function name as an atom. To try the plug out, go back to your browser and fetch [http://localhost:4000](http://localhost:4000). You should see something like this printed in your shell terminal:
```console
Verb: "GET"
Host: "localhost"
Headers: [...]
```
Our plug simply prints information from the connection. Although our initial plug is very simple, you can do virtually anything you want inside of it. To learn about all fields available in the connection and all of the functionality associated to it, see the [documentation for `Plug.Conn`](https://hexdocs.pm/plug/Plug.Conn.html).
Now let's look at the other plug variant, the module plugs.
## Module plugs
Module plugs are another type of plug that let us define a connection transformation in a module. The module only needs to implement two functions:
- [`init/1`] which initializes any arguments or options to be passed to [`call/2`]
- [`call/2`] which carries out the connection transformation. [`call/2`] is just a function plug that we saw earlier
To see this in action, let's write a module plug that puts the `:locale` key and value into the connection for downstream use in other plugs, controller actions, and our views. Put the contents below in a file named `lib/hello_web/plugs/locale.ex`:
```elixir
defmodule HelloWeb.Plugs.Locale do
import Plug.Conn
@locales ["en", "fr", "de"]
def init(default), do: default
def call(%Plug.Conn{params: %{"locale" => loc}} = conn, _default) when loc in @locales do
assign(conn, :locale, loc)
end
def call(conn, default) do
assign(conn, :locale, default)
end
end
```
To give it a try, let's add this module plug to our router, by appending `plug HelloWeb.Plugs.Locale, "en"` to our `:browser` pipeline in `lib/hello_web/router.ex`:
```elixir
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug HelloWeb.Plugs.Locale, "en"
end
...
```
In the [`init/1`] callback, we pass a default locale to use if none is present in the params. We also use pattern matching to define multiple [`call/2`] function heads to validate the locale in the params, and fall back to `"en"` if there is no match. The [`assign/3`] is a part of the `Plug.Conn` module and it's how we store values in the `conn` data structure.
To see the assign in action, go to the template in `lib/hello_web/controllers/page_html/home.html.heex` and add the following code after the closing of the `` tag:
```heex
Locale: {@locale}
```
Go to [http://localhost:4000/](http://localhost:4000/) and you should see the locale exhibited. Visit [http://localhost:4000/?locale=fr](http://localhost:4000/?locale=fr) and you should see the assign changed to `"fr"`. You can use this information alongside [Gettext](https://hexdocs.pm/gettext/Gettext.html) to provide a fully internationalized web application.
That's all there is to Plug. Phoenix embraces the plug design of composable transformations all the way up and down the stack. Let's see some examples!
## Where to plug
The endpoint, router, and controllers in Phoenix accept plugs.
### Endpoint plugs
Endpoints organize all the plugs common to every request, and apply them before dispatching into the router with its custom pipelines. We added a plug to the endpoint like this:
```elixir
defmodule HelloWeb.Endpoint do
...
plug :introspect
plug HelloWeb.Router
```
The default endpoint plugs do quite a lot of work. Here they are in order:
- `Plug.Static` - serves static assets. Since this plug comes before the logger, requests for static assets are not logged.
- `Phoenix.LiveDashboard.RequestLogger` - sets up the *Request Logger* for Phoenix LiveDashboard, this will allow you to have the option to either pass a query parameter to stream requests logs or to enable/disable a cookie that streams requests logs from your dashboard.
- `Plug.RequestId` - generates a unique request ID for each request.
- `Plug.Telemetry` - adds instrumentation points so Phoenix can log the request path, status code and request time by default.
- `Plug.Parsers` - parses the request body when a known parser is available. By default, this plug can handle URL-encoded, multipart and JSON content (with `Jason`). The request body is left untouched if the request content-type cannot be parsed.
- `Plug.MethodOverride` - converts the request method to PUT, PATCH or DELETE for POST requests with a valid `_method` parameter.
- `Plug.Head` - converts HEAD requests to GET requests.
- `Plug.Session` - a plug that sets up session management. Note that `fetch_session/2` must still be explicitly called before using the session, as this plug just sets up how the session is fetched.
In the middle of the endpoint, there is also a conditional block:
```elixir
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :hello
end
```
This block is only executed in development. It enables:
* live reloading - if you change a CSS file, they are updated in-browser without refreshing the page;
* [code reloading](`Phoenix.CodeReloader`) - so we can see changes to our application without restarting the server;
* check repo status - which makes sure our database is up to date, raising a readable and actionable error otherwise.
### Router plugs
In the router, we can declare plugs inside pipelines:
```elixir
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug HelloWeb.Plugs.Locale, "en"
end
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :index
end
```
Routes are defined inside scopes and scopes may pipe through multiple pipelines. Once a route matches, Phoenix invokes all plugs defined in all pipelines associated to that route. For example, accessing "/" will pipe through the `:browser` pipeline, consequently invoking all of its plugs.
As we will see in the [routing guide](routing.html), the pipelines themselves are plugs. There, we will also discuss all plugs in the `:browser` pipeline.
### Controller plugs
Finally, controllers are plugs too, so we can do:
```elixir
defmodule HelloWeb.PageController do
use HelloWeb, :controller
plug HelloWeb.Plugs.Locale, "en"
```
In particular, controller plugs provide a feature that allows us to execute plugs only within certain actions. For example, you can do:
```elixir
defmodule HelloWeb.PageController do
use HelloWeb, :controller
plug HelloWeb.Plugs.Locale, "en" when action in [:index]
```
And the plug will only be executed for the `index` action.
## Plugs as composition
By abiding by the plug contract, we turn an application request into a series of explicit transformations. It doesn't stop there. To really see how effective Plug's design is, let's imagine a scenario where we need to check a series of conditions and then either redirect or halt if a condition fails. Without plug, we would end up with something like this:
```elixir
defmodule HelloWeb.MessageController do
use HelloWeb, :controller
def show(conn, params) do
case Authenticator.find_user(conn) do
{:ok, user} ->
case find_message(params["id"]) do
nil ->
conn |> put_flash(:info, "That message wasn't found") |> redirect(to: ~p"/")
message ->
if Authorizer.can_access?(user, message) do
render(conn, :show, page: message)
else
conn |> put_flash(:info, "You can't access that page") |> redirect(to: ~p"/")
end
end
:error ->
conn |> put_flash(:info, "You must be logged in") |> redirect(to: ~p"/")
end
end
end
```
Notice how just a few steps of authentication and authorization require complicated nesting and duplication? Let's improve this with a couple of plugs.
```elixir
defmodule HelloWeb.MessageController do
use HelloWeb, :controller
plug :authenticate
plug :fetch_message
plug :authorize_message
def show(conn, params) do
render(conn, :show, page: conn.assigns[:message])
end
defp authenticate(conn, _) do
case Authenticator.find_user(conn) do
{:ok, user} ->
assign(conn, :user, user)
:error ->
conn |> put_flash(:info, "You must be logged in") |> redirect(to: ~p"/") |> halt()
end
end
defp fetch_message(conn, _) do
case find_message(conn.params["id"]) do
nil ->
conn |> put_flash(:info, "That message wasn't found") |> redirect(to: ~p"/") |> halt()
message ->
assign(conn, :message, message)
end
end
defp authorize_message(conn, _) do
if Authorizer.can_access?(conn.assigns[:user], conn.assigns[:message]) do
conn
else
conn |> put_flash(:info, "You can't access that page") |> redirect(to: ~p"/") |> halt()
end
end
end
```
To make this all work, we converted the nested blocks of code and used `halt(conn)` whenever we reached a failure path. The `halt(conn)` functionality is essential here: it tells Plug that the next plug should not be invoked.
At the end of the day, by replacing the nested blocks of code with a flattened series of plug transformations, we are able to achieve the same functionality in a much more composable, clear, and reusable way.
To learn more about plugs, see the documentation for the [Plug project](`Plug`), which provides many built-in plugs and functionalities.
[`init/1`]: `c:Plug.init/1`
[`call/2`]: `c:Plug.call/2`
[`assign/3`]: `Plug.Conn.assign/3`
================================================
FILE: guides/real_time/channels.md
================================================
# Channels
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
Channels are an exciting part of Phoenix that enable soft real-time communication with and between millions of connected clients.
Some possible use cases include:
- Chat rooms and APIs for messaging apps
- Breaking news, like "a goal was scored" or "an earthquake is coming"
- Tracking trains, trucks, or race participants on a map
- Events in multiplayer games
- Monitoring sensors and controlling lights
- Notifying a browser that a page's CSS or JavaScript has changed (this is handy in development)
Conceptually, Channels are pretty simple.
First, clients connect to the server using some transport, like WebSocket. Once connected, they join one or more topics. For example, to interact with a public chat room clients may join a topic called `public_chat`, and to receive updates from a product with ID 7, they may need to join a topic called `product_updates:7`.
Clients can push messages to the topics they've joined, and can also receive messages from them. The other way around, Channel servers receive messages from their connected clients, and can push messages to them too.
Servers are able to broadcast messages to all clients subscribed to a certain topic. This is illustrated in the following diagram:
```plaintext
+----------------+
+--Topic X-->| Mobile Client |
| +----------------+
+-------------------+ |
+----------------+ | | | +----------------+
| Browser Client |--Topic X-->| Phoenix Server(s) |--+--Topic X-->| Desktop Client |
+----------------+ | | | +----------------+
+-------------------+ |
| +----------------+
+--Topic X-->| IoT Client |
+----------------+
```
Broadcasts work even if the application runs on several nodes/computers. That is, if two clients have their socket connected to different application nodes and are subscribed to the same topic `T`, both of them will receive messages broadcasted to `T`. That is possible thanks to an internal PubSub mechanism.
Channels can support any kind of client: a browser, native app, smart watch, embedded device, or anything else that can connect to a network.
All the client needs is a suitable library; see the [Client Libraries](#client-libraries) section below.
Each client library communicates using one of the "transports" that Channels understand.
Currently, that's either Websockets or long polling, but other transports may be added in the future.
Unlike stateless HTTP connections, Channels support long-lived connections, each backed by a lightweight Erlang VM process, working in parallel and maintaining its own state.
This architecture scales well; Phoenix Channels [can support millions of subscribers with reasonable latency on a single box](https://phoenixframework.org/blog/the-road-to-2-million-websocket-connections), passing hundreds of thousands of messages per second.
And that capacity can be multiplied by adding more nodes to the cluster.
## The Moving Parts
Although Channels are simple to use from a client perspective, there are a number of components involved in routing messages to clients across a cluster of servers.
Let's take a look at them.
### Overview
To start communicating, a client connects to a node (a Phoenix server) using a transport (e.g., Websockets or long polling) and joins one or more channels using that single network connection.
One channel server lightweight process is created per client, per topic. Each channel holds onto the `%Phoenix.Socket{}` and can maintain any state it needs within its `socket.assigns`.
Once the connection is established, each incoming message from a client is routed, based on its topic, to the correct channel server.
If the channel server asks to broadcast a message, that message is sent to the local PubSub, which sends it out to any clients connected to the same server and subscribed to that topic.
If there are other nodes in the cluster, the local PubSub also forwards the message to their PubSubs, which send it out to their own subscribers.
Because only one message has to be sent per additional node, the performance cost of adding nodes is negligible, while each new node supports many more subscribers.
The message flow looks something like this:
```plaintext
Channel +-------------------------+ +--------+
route | Sending Client, Topic 1 | | Local |
+----------->| Channel.Server |----->| PubSub |--+
+----------------+ | +-------------------------+ +--------+ |
| Sending Client |-Transport--+ | |
+----------------+ +-------------------------+ | |
| Sending Client, Topic 2 | | |
| Channel.Server | | |
+-------------------------+ | |
| |
+-------------------------+ | |
+----------------+ | Browser Client, Topic 1 | | |
| Browser Client |<-------Transport--------| Channel.Server |<----------+ |
+----------------+ +-------------------------+ |
|
|
|
+-------------------------+ |
+----------------+ | Phone Client, Topic 1 | |
| Phone Client |<-------Transport--------| Channel.Server |<-+ |
+----------------+ +-------------------------+ | +--------+ |
| | Remote | |
+-------------------------+ +---| PubSub |<-+
+----------------+ | Watch Client, Topic 1 | | +--------+ |
| Watch Client |<-------Transport--------| Channel.Server |<-+ |
+----------------+ +-------------------------+ |
|
|
+-------------------------+ +--------+ |
+----------------+ | IoT Client, Topic 1 | | Remote | |
| IoT Client |<-------Transport--------| Channel.Server |<-----| PubSub |<-+
+----------------+ +-------------------------+ +--------+
```
### Endpoint
In your Phoenix app's `Endpoint` module, a `socket` declaration specifies which socket handler will receive connections on a given URL.
```elixir
socket "/socket", HelloWeb.UserSocket,
websocket: true,
longpoll: false,
auth_token: true
```
Phoenix comes with two default transports: websocket and longpoll. You can configure them directly via the `socket` declaration.
### Socket Handlers
On the client side, you will establish a socket connection to the route above:
```javascript
let socket = new Socket("/socket", {authToken: window.userToken})
```
On the server, Phoenix will invoke `HelloWeb.UserSocket.connect/2`, passing your parameters and the initial socket state. Within the socket, you can authenticate and identify a socket connection and set default socket assigns. The socket is also where you define your channel routes.
### Channel Routes
Channel routes match on the topic string and dispatch matching requests to the given Channel module.
The star character `*` acts as a wildcard matcher, so in the following example route, requests for `room:lobby` and `room:123` would both be dispatched to the `RoomChannel`. In your `UserSocket`, you would have:
```elixir
channel "room:*", HelloWeb.RoomChannel
```
### Channels
Channels handle events from clients, so they are similar to Controllers, but there are two key differences. Channel events can go both directions - incoming and outgoing. Channel connections also persist beyond a single request/response cycle. Channels are the highest level abstraction for real-time communication components in Phoenix.
Each Channel will implement one or more clauses of each of these four callback functions - `join/3`, `terminate/2`, `handle_in/3`, and `handle_out/3`.
### Topics
Topics are string identifiers - names that the various layers use in order to make sure messages end up in the right place. As we saw above, topics can use wildcards. This allows for a useful `"topic:subtopic"` convention. Often, you'll compose topics using record IDs from your application layer, such as `"users:123"`.
### Messages
The `Phoenix.Socket.Message` module defines a struct with the following keys which denotes a valid message. From the [Phoenix.Socket.Message docs](https://hexdocs.pm/phoenix/Phoenix.Socket.Message.html).
- `topic` - The string topic or `"topic:subtopic"` pair namespace, such as `"messages"` or `"messages:123"`
- `event` - The string event name, for example `"phx_join"`
- `payload` - The message payload
- `ref` - The unique string ref
### PubSub
PubSub is provided by the `Phoenix.PubSub` module. Interested parties can receive events by subscribing to topics. Other processes can broadcast events to certain topics.
This is useful to broadcast messages on channel and also for application development in general. For instance, letting all connected [live views](https://github.com/phoenixframework/phoenix_live_view) to know that a new comment has been added to a post.
The PubSub system takes care of getting messages from one node to another so that they can be sent to all subscribers across the cluster.
By default, this is done using [Phoenix.PubSub.PG2](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.PG2.html), which uses native Erlang VM messaging.
If your deployment environment does not support distributed Elixir or direct communication between servers, Phoenix also ships with a [Redis Adapter](https://hexdocs.pm/phoenix_pubsub_redis/Phoenix.PubSub.Redis.html) that uses Redis to exchange PubSub data. Please see the [Phoenix.PubSub docs](https://hexdocs.pm/phoenix_pubsub/Phoenix.PubSub.html) for more information.
### Client Libraries
Any networked device can connect to Phoenix Channels as long as it has a client library.
The following libraries exist today, and new ones are always welcome; to write your own, see our how-to guide [Writing a Channels Client](writing_a_channels_client.md).
#### Official
Phoenix ships with a JavaScript client that is available when generating a new Phoenix project. The documentation for the JavaScript module is available at [https://hexdocs.pm/phoenix/js/](https://hexdocs.pm/phoenix/js/); the code is in [multiple js files](https://github.com/phoenixframework/phoenix/blob/main/assets/js/phoenix/).
#### 3rd Party
+ Swift (iOS)
- [SwiftPhoenix](https://github.com/davidstump/SwiftPhoenixClient)
+ Java (Android)
- [JavaPhoenixChannels](https://github.com/eoinsha/JavaPhoenixChannels)
+ Kotlin (Android)
- [JavaPhoenixClient](https://github.com/dsrees/JavaPhoenixClient)
+ C#
- [PhoenixSharp](https://github.com/Mazyod/PhoenixSharp)
+ Elixir
- [phoenix_gen_socket_client](https://github.com/Aircloak/phoenix_gen_socket_client)
- [slipstream](https://hexdocs.pm/slipstream/Slipstream.html)
+ GDScript (Godot Game Engine)
- [GodotPhoenixChannels](https://github.com/alfredbaudisch/GodotPhoenixChannels)
## Tying it all together
Let's tie all these ideas together by building a simple chat application. Make sure [you created a new Phoenix application](https://hexdocs.pm/phoenix/up_and_running.html) and now we are ready to generate the `UserSocket`.
### Generating a socket
Let's invoke the socket generator to get started:
```console
$ mix phx.gen.socket User
```
It will create two files, the client code in `assets/js/user_socket.js` and the server counter-part in `lib/hello_web/channels/user_socket.ex`. After running, the generator will also ask to add the following line to `lib/hello_web/endpoint.ex`:
```elixir
defmodule HelloWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :hello
socket "/socket", HelloWeb.UserSocket,
websocket: true,
longpoll: false
...
end
```
The generator also asks us to import the client code, we will do that later.
Next, we will configure our socket to ensure messages get routed to the correct channel. To do that, we'll uncomment the `"room:*"` channel definition:
```elixir
defmodule HelloWeb.UserSocket do
use Phoenix.Socket
## Channels
channel "room:*", HelloWeb.RoomChannel
...
```
Now, whenever a client sends a message whose topic starts with `"room:"`, it will be routed to our RoomChannel. Next, we'll define a `HelloWeb.RoomChannel` module to manage our chat room messages.
### Joining Channels
The first priority of your channels is to authorize clients to join a given topic. For authorization, we must implement `join/3` in `lib/hello_web/channels/room_channel.ex`.
```elixir
defmodule HelloWeb.RoomChannel do
use Phoenix.Channel
def join("room:lobby", _message, socket) do
{:ok, socket}
end
def join("room:" <> _private_room_id, _params, _socket) do
{:error, %{reason: "unauthorized"}}
end
end
```
For our chat app, we'll allow anyone to join the `"room:lobby"` topic, but any other room will be considered private and special authorization, say from a database, will be required.
(We won't worry about private chat rooms for this exercise, but feel free to explore after we finish.)
With our channel in place, let's get the client and server talking.
The generated `assets/js/user_socket.js` defines a simple client based on the socket implementation that ships with Phoenix.
We can use that library to connect to our socket and join our channel, we just need to set our room name to `"room:lobby"` in that file.
```javascript
// assets/js/user_socket.js
// ...
socket.connect()
// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("room:lobby", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket
```
After that, we need to make sure `assets/js/user_socket.js` gets imported into our application JavaScript file. To do that, uncomment this line in `assets/js/app.js`.
```javascript
// ...
import "./user_socket.js"
```
Save the file and your browser should auto refresh, thanks to the Phoenix live reloader. If everything worked, we should see "Joined successfully" in the browser's JavaScript console. Our client and server are now talking over a persistent connection. Now let's make it useful by enabling chat.
In `lib/hello_web/controllers/page_html/home.html.heex`, we'll replace the existing code with a container to hold our chat messages, and an input field to send them:
```heex
```
Now let's add a couple of event listeners to `assets/js/user_socket.js`:
```javascript
// ...
let channel = socket.channel("room:lobby", {})
let chatInput = document.querySelector("#chat-input")
let messagesContainer = document.querySelector("#messages")
chatInput.addEventListener("keypress", event => {
if(event.key === 'Enter'){
channel.push("new_msg", {body: chatInput.value})
chatInput.value = ""
}
})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket
```
All we had to do is detect that enter was pressed and then `push` an event over the channel with the message body. We named the event `"new_msg"`. With this in place, let's handle the other piece of a chat application, where we listen for new messages and append them to our messages container.
```javascript
// ...
let channel = socket.channel("room:lobby", {})
let chatInput = document.querySelector("#chat-input")
let messagesContainer = document.querySelector("#messages")
chatInput.addEventListener("keypress", event => {
if(event.key === 'Enter'){
channel.push("new_msg", {body: chatInput.value})
chatInput.value = ""
}
})
channel.on("new_msg", payload => {
let messageItem = document.createElement("p")
messageItem.innerText = `[${Date()}] ${payload.body}`
messagesContainer.appendChild(messageItem)
})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket
```
We listen for the `"new_msg"` event using `channel.on`, and then append the message body to the DOM. Now let's handle the incoming and outgoing events on the server to complete the picture.
### Incoming Events
We handle incoming events with `handle_in/3`. We can pattern match on the event names, like `"new_msg"`, and then grab the payload that the client passed over the channel. For our chat application, we simply need to notify all other `room:lobby` subscribers of the new message with `broadcast!/3`.
```elixir
defmodule HelloWeb.RoomChannel do
use Phoenix.Channel
def join("room:lobby", _message, socket) do
{:ok, socket}
end
def join("room:" <> _private_room_id, _params, _socket) do
{:error, %{reason: "unauthorized"}}
end
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast!(socket, "new_msg", %{body: body})
{:noreply, socket}
end
end
```
`broadcast!/3` will notify all joined clients on this `socket`'s topic and invoke their `handle_out/3` callbacks. `handle_out/3` isn't a required callback, but it allows us to customize and filter broadcasts before they reach each client. By default, `handle_out/3` is implemented for us and simply pushes the message on to the client. Hooking into outgoing events allows for powerful message customization and filtering. Let's see how.
### Intercepting Outgoing Events
We won't implement this for our application, but imagine our chat app allowed users to ignore messages about new users joining a room. We could implement that behavior like this, where we explicitly tell Phoenix which outgoing event we want to intercept and then define a `handle_out/3` callback for those events. (Of course, this assumes that we have an `Accounts` context with an `ignoring_user?/2` function, and that we pass a user in via the `assigns` map). It is important to note that the `handle_out/3` callback will be called for every recipient of a message, so more expensive operations like hitting the database should be considered carefully before being included in `handle_out/3`.
```elixir
intercept ["user_joined"]
def handle_out("user_joined", msg, socket) do
if Accounts.ignoring_user?(socket.assigns[:user], msg.user_id) do
{:noreply, socket}
else
push(socket, "user_joined", msg)
{:noreply, socket}
end
end
```
That's all there is to our basic chat app. Fire up multiple browser tabs and you should see your messages being pushed and broadcasted to all windows!
## Using Token Authentication
When we connect, we'll often need to authenticate the client. Fortunately, this is a 5-step process with [Phoenix.Token](https://hexdocs.pm/phoenix/Phoenix.Token.html).
### Step 1 - Enable the `auth_token` functionality in the socket
Phoenix supports a transport agnostic way to pass an authentication token to the server. To enable this, we need to pass the `:auth_token` option to the socket declaration in our `Endpoint` module.
```elixir
defmodule HelloWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :hello
socket "/socket", HelloWeb.UserSocket,
websocket: true,
longpoll: false,
auth_token: true
...
end
```
### Step 2 - Assign a Token in the Connection
Let's say we have an authentication plug in our app called `OurAuth`. When `OurAuth` authenticates a user, it sets a value for the `:current_user` key in `conn.assigns`. Since the `current_user` exists, we can simply assign the user's token in the connection for use in the layout. We can wrap that behavior up in a private function plug, `put_user_token/2`. This could also be put in its own module as well. To make this all work, we just add `OurAuth` and `put_user_token/2` to the browser pipeline.
```elixir
pipeline :browser do
...
plug OurAuth
plug :put_user_token
end
defp put_user_token(conn, _) do
if current_user = conn.assigns[:current_user] do
token = Phoenix.Token.sign(conn, "user socket", current_user.id)
assign(conn, :user_token, token)
else
conn
end
end
```
Now our `conn.assigns` contains the `current_user` and `user_token`.
### Step 3 - Pass the Token to the JavaScript
Next, we need to pass this token to JavaScript. We can do so inside a script tag in `lib/hello_web/components/layouts/root.html.heex` right above the app.js script, as follows:
```heex
```
### Step 4 - Pass the Token to the Socket Constructor and Verify
We also need to pass the `:auth_token` to the socket constructor and verify the user token in the `connect/3` function. To do so, edit `lib/hello_web/channels/user_socket.ex`, as follows:
```elixir
def connect(_params_, socket, connect_info) do
# max_age: 1209600 is equivalent to two weeks in seconds
case Phoenix.Token.verify(socket, "user socket", connect_info[:auth_token], max_age: 1209600) do
{:ok, user_id} ->
{:ok, assign(socket, :current_user, user_id)}
{:error, reason} ->
:error
end
end
```
In our JavaScript, we can use the token set previously when constructing the Socket:
```javascript
let socket = new Socket("/socket", {authToken: window.userToken})
```
We used `Phoenix.Token.verify/4` to verify the user token provided by the client. `Phoenix.Token.verify/4` returns either `{:ok, user_id}` or `{:error, reason}`. We can pattern match on that return in a `case` statement. With a verified token, we set the user's id as the value to `:current_user` in the socket. Otherwise, we return `:error`.
### Step 5 - Connect to the socket in JavaScript
With authentication set up, we can connect to sockets and channels from JavaScript.
```javascript
let socket = new Socket("/socket", {authToken: window.userToken})
socket.connect()
```
Now that we are connected, we can join channels with a topic:
```javascript
let channel = socket.channel("topic:subtopic", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket
```
Note that token authentication is preferable since it's transport agnostic and well-suited for long running-connections like channels, as opposed to using sessions or other authentication approaches.
## Fault Tolerance and Reliability Guarantees
Servers restart, networks split, and clients lose connectivity. In order to design robust systems, we need to understand how Phoenix responds to these events and what guarantees it offers.
### Handling Reconnection
Clients subscribe to topics, and Phoenix stores those subscriptions in an in-memory ETS table. If a channel crashes, the clients will need to reconnect to the topics they had previously subscribed to. Fortunately, the Phoenix JavaScript client knows how to do this. The server will notify all the clients of the crash. This will trigger each client's `Channel.onError` callback. The clients will attempt to reconnect to the server using an exponential backoff strategy. Once they reconnect, they'll attempt to rejoin the topics they had previously subscribed to. If they are successful, they'll start receiving messages from those topics as before.
### Resending Client Messages
Channel clients queue outgoing messages into a `PushBuffer`, and send them to the server when there is a connection. If no connection is available, the client holds on to the messages until it can establish a new connection. With no connection, the client will hold the messages in memory until it establishes a connection, or until it receives a `timeout` event. The default timeout is set to 5000 milliseconds. The client won't persist the messages in the browser's local storage, so if the browser tab closes, the messages will be gone.
### Resending Server Messages
Phoenix uses an at-most-once strategy when sending messages to clients. If the client is offline and misses the message, Phoenix won't resend it. Phoenix doesn't persist messages on the server. If the server restarts, unsent messages will be gone. If our application needs stronger guarantees around message delivery, we'll need to write that code ourselves. Common approaches involve persisting messages on the server and having clients request missing messages.
For example, you can track a `last_seen_id` (or `last_updated_at`) on the client. On join, the client will pass the `last_seen_id` via the channel params for the last thing it saw, which lets the server know how to catch the client up with side effects that have happened since that id. On the client, anytime an event is received for a "new_message", the client bumps its `last_seen_id` so that it can recover gracefully across disconnect/reconnect. The code might look something like this:
```javascript
// on the client
let socket = new Socket(...)
let lastSeenMsg = {}
let roomParams = () => lastSeenMsg.id ? {last_seen_id: lastSeenMsg.id} : {}
let roomChannel = socket.channel("rooms:123", roomParams)
roomChannel.on("new_message", msg => {
lastSeenMsg = msg
renderNewMessage(msg)
})
roomChannel.join().receive("ok", ({messages}) => {
lastSeenMsg = messages[messages.length - 1]
renderMessages(messages)
})
```
```elixir
# room_channel.ex on the server
def join("rooms:" <> id, params, socket) do
...
messages = fetch_messages_since(params["last_seen_id"])
{:ok, %{messages: messages}, socket}
end
```
## Example Application
To see an example of the application we just built, checkout the project [phoenix_chat_example](https://github.com/chrismccord/phoenix_chat_example).
================================================
FILE: guides/real_time/presence.md
================================================
# Presence
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
> **Requirement**: This guide expects that you have gone through the [Channels guide](channels.html).
Phoenix Presence is a feature which allows you to register process information on a topic and replicate it transparently across a cluster. It's a combination of both a server-side and client-side library, which makes it simple to implement. A simple use-case would be showing which users are currently online in an application.
Phoenix Presence is special for a number of reasons. It has no single point of failure, no single source of truth, relies entirely on the standard library with no operational dependencies and self-heals.
## Setting up
We are going to use Presence to track which users are connected on the server and send updates to the client as users join and leave. We will deliver those updates via Phoenix Channels. Therefore, let's create a `RoomChannel`, as we did in the channels guides:
```console
$ mix phx.gen.channel Room
```
Follow the steps after the generator and you are ready to start tracking presence.
## The Presence generator
To get started with Presence, we'll first need to generate a presence module. We can do this with the `mix phx.gen.presence` task:
```console
$ mix phx.gen.presence
* creating lib/hello_web/channels/presence.ex
Add your new module to your supervision tree,
in lib/hello/application.ex:
children = [
...
HelloWeb.Presence,
]
You're all set! See the Phoenix.Presence docs for more details:
https://hexdocs.pm/phoenix/Phoenix.Presence.html
```
If we open up the `lib/hello_web/channels/presence.ex` file, we will see the following line:
```elixir
use Phoenix.Presence,
otp_app: :hello,
pubsub_server: Hello.PubSub
```
This sets up the module for presence, defining the functions we require for tracking presences. As mentioned in the generator task, we should add this module to our supervision tree in
`application.ex`:
```elixir
children = [
...
HelloWeb.Presence,
]
```
## Usage With Channels and JavaScript
Next, we will create the channel that we'll communicate presence over. After a user joins, we can push the list of presences down the channel and then track the connection. We can also provide a map of additional information to track.
```elixir
defmodule HelloWeb.RoomChannel do
use Phoenix.Channel
alias HelloWeb.Presence
def join("room:lobby", %{"name" => name}, socket) do
send(self(), :after_join)
{:ok, assign(socket, :name, name)}
end
def handle_info(:after_join, socket) do
{:ok, _} =
Presence.track(socket, socket.assigns.name, %{
online_at: inspect(System.system_time(:second))
})
push(socket, "presence_state", Presence.list(socket))
{:noreply, socket}
end
end
```
Finally, we can use the client-side Presence library included in `phoenix.js` to manage the state and presence diffs that come down the socket. It listens for the `"presence_state"` and `"presence_diff"` events and provides a simple callback for you to handle the events as they happen, with the `onSync` callback.
The `onSync` callback allows you to easily react to presence state changes, which most often results in re-rendering an updated list of active users. You can use the `list` method to format and return each individual presence based on the needs of your application.
To iterate users, we use the `presences.list()` function which accepts a callback. The callback will be called for each presence item with 2 arguments, the presence id and a list of metas (one for each presence for that presence id). We use this to display the users and the number of devices they are online with.
We can see presence working by adding the following to `assets/js/app.js`:
```javascript
import {Socket, Presence} from "phoenix"
let socket = new Socket("/socket", {authToken: window.userToken})
let channel = socket.channel("room:lobby", {name: window.location.search.split("=")[1]})
let presence = new Presence(channel)
function renderOnlineUsers(presence) {
let response = ""
presence.list((id, {metas: [first, ...rest]}) => {
let count = rest.length + 1
response += ` ${id} (count: ${count})`
})
document.querySelector("main").innerHTML = response
}
socket.connect()
presence.onSync(() => renderOnlineUsers(presence))
channel.join()
```
We can ensure this is working by opening 3 browser tabs. If we navigate to on two browser tabs and then we should see:
```plaintext
Alice (count: 2)
Bob (count: 1)
```
If we close one of the Alice tabs, then the count should decrease to 1. If we close another tab, the user should disappear from the list entirely.
### Making it safe
In our initial implementation, we are passing the name of the user as part of the URL. However, in many systems, you want to allow only logged in users to access the presence functionality. To do so, you should set up token authentication, [as detailed in the token authentication section of the channels guide](channels.html#using-token-authentication).
With token authentication, you should access `socket.assigns.user_id`, set in `UserSocket`, instead of `socket.assigns.name` set from parameters.
## Usage With LiveView
Whilst Phoenix does ship with a JavaScript API for dealing with presence, it is also possible to extend the `HelloWeb.Presence` module to support [LiveView](https://hexdocs.pm/phoenix_live_view).
One thing to keep in mind when dealing with LiveView, is that each LiveView is a stateful process, so if we keep the presence state in the LiveView, each LiveView process will contain the full list of online users in memory. Instead, we can keep track of the online users within the `Presence` process, and pass separate events to the LiveView, which can use a stream to update the online list.
To start with, we need to update the `lib/hello_web/channels/presence.ex` file to add some optional callbacks to the `HelloWeb.Presence` module.
Firstly, we add the `init/1` callback. This allows us to keep track of the presence state within the process.
```elixir
def init(_opts) do
{:ok, %{}}
end
```
The presence module also allows a `fetch/2` callback, this allows the data fetched from the presence to be modified, allowing us to define the shape of the response. In this case we are adding an `id` and a `user` map.
```elixir
def fetch(_topic, presences) do
for {key, %{metas: [meta | metas]}} <- presences, into: %{} do
# user can be populated here from the database here we populate
# the name for demonstration purposes
{key, %{metas: [meta | metas], id: meta.id, user: %{name: meta.id}}}
end
end
```
The final thing to add is the `handle_metas/4` callback. This callback updates the state that we keep track of in `HelloWeb.Presence` based on the user leaves and joins.
```elixir
def handle_metas(topic, %{joins: joins, leaves: leaves}, presences, state) do
for {user_id, presence} <- joins do
user_data = %{id: user_id, user: presence.user, metas: Map.fetch!(presences, user_id)}
msg = {__MODULE__, {:join, user_data}}
Phoenix.PubSub.local_broadcast(Hello.PubSub, "proxy:#{topic}", msg)
end
for {user_id, presence} <- leaves do
metas =
case Map.fetch(presences, user_id) do
{:ok, presence_metas} -> presence_metas
:error -> []
end
user_data = %{id: user_id, user: presence.user, metas: metas}
msg = {__MODULE__, {:leave, user_data}}
Phoenix.PubSub.local_broadcast(Hello.PubSub, "proxy:#{topic}", msg)
end
{:ok, state}
end
```
You can see that we are broadcasting events for the joins and leaves. These will be listened to by the LiveView process. You'll also see that we use "proxy" channel when broadcasting the joins and leaves. This is because we don't want our LiveView process to receive the presence events directly. We can add a few helper functions so that this particular implementation detail is abstracted from the LiveView module.
```elixir
def list_online_users(), do: list("online_users") |> Enum.map(fn {_id, presence} -> presence end)
def track_user(name, params), do: track(self(), "online_users", name, params)
def subscribe(), do: Phoenix.PubSub.subscribe(Hello.PubSub, "proxy:online_users")
```
Now that we have our presence module set up and broadcasting events, we can create a LiveView. Create a new file `lib/hello_web/live/online/index.ex` with the following contents:
```elixir
defmodule HelloWeb.OnlineLive do
use HelloWeb, :live_view
def mount(params, _session, socket) do
socket = stream(socket, :presences, [])
socket =
if connected?(socket) do
HelloWeb.Presence.track_user(params["name"], %{id: params["name"]})
HelloWeb.Presence.subscribe()
stream(socket, :presences, HelloWeb.Presence.list_online_users())
else
socket
end
{:ok, socket}
end
def render(assigns) do
~H"""
{id} ({length(metas)})
"""
end
def handle_info({HelloWeb.Presence, {:join, presence}}, socket) do
{:noreply, stream_insert(socket, :presences, presence)}
end
def handle_info({HelloWeb.Presence, {:leave, presence}}, socket) do
if presence.metas == [] do
{:noreply, stream_delete(socket, :presences, presence)}
else
{:noreply, stream_insert(socket, :presences, presence)}
end
end
end
```
If we add this route to the `lib/hello_web/router.ex`:
```elixir
live "/online/:name", OnlineLive, :index
```
Then we can navigate to http://localhost:4000/online/Alice in one tab, and http://localhost:4000/online/Bob in another, you'll see that the presences are tracked, along with the number of presences per user. Opening and closing tabs with various users will update the presence list in real-time.
================================================
FILE: guides/request_lifecycle.md
================================================
# Request life-cycle
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
The goal of this guide is to talk about Phoenix's request life-cycle. This guide will take a practical approach where we will learn by doing: we will add two new pages to our Phoenix project and comment on how the pieces fit together along the way.
Let's get on with our first new Phoenix page!
## Adding a new page
When your browser accesses [http://localhost:4000/](http://localhost:4000/), it sends an HTTP request to whatever service is running on that address, in this case our Phoenix application. The HTTP request is made of a verb and a path. For example, the following browser requests translate into:
| Browser address bar | Verb | Path |
|:------------------------------------|:-----|:--------------|
| | GET | / |
| | GET | /hello |
| | GET | /hello/world |
There are other HTTP verbs. For example, submitting a form typically uses the POST verb.
Web applications typically handle requests by mapping each verb/path pair onto a specific part of your application. In Phoenix, this mapping is done by the router. For example, we may map "/articles" to a portion of our application that shows all articles. Therefore, to add a new page, our first task is to add a new route.
### A new route
The router maps unique HTTP verb/path pairs to controller/action pairs which will handle them. Controllers in Phoenix are simply Elixir modules. Actions are functions that are defined within these controllers.
Phoenix generates a router file for us in new applications at `lib/hello_web/router.ex`. This is where we will be working for this section.
The route for our "Welcome to Phoenix!" page from the previous [Up And Running Guide](up_and_running.html) looks like this:
```elixir
get "/", PageController, :home
```
Let's digest what this route is telling us. Visiting [http://localhost:4000/](http://localhost:4000/) issues an HTTP `GET` request to the root path. All requests like this will be handled by the `home/2` function in the `HelloWeb.PageController` module defined in `lib/hello_web/controllers/page_controller.ex`.
The page we are going to build will say "Hello World, from Phoenix!" when we point our browser to [http://localhost:4000/hello](http://localhost:4000/hello).
The first thing we need to do is to create the page route for a new page. Let's open up `lib/hello_web/router.ex` in a text editor. For a brand new application, it looks like this:
```elixir
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
end
# Other scopes may use custom stacks.
# scope "/api", HelloWeb do
# pipe_through :api
# end
# ...
end
```
For now, we'll ignore the pipelines and the use of `scope` here and just focus on adding a route. We will discuss those in the [Routing guide](routing.html).
Let's add a new route to the router that maps a `GET` request for `/hello` to the `index` action of a soon-to-be-created `HelloWeb.HelloController` inside the `scope "/" do` block of the router:
```elixir
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
get "/hello", HelloController, :index
end
```
### A new controller
Controllers are Elixir modules, and actions are Elixir functions defined in them. The purpose of actions is to gather the data and perform the tasks needed for rendering. Our route specifies that we need a `HelloWeb.HelloController` module with an `index/2` function.
To make the `index` action happen, let's create a new `lib/hello_web/controllers/hello_controller.ex` file, and make it look like the following:
```elixir
defmodule HelloWeb.HelloController do
use HelloWeb, :controller
def index(conn, _params) do
render(conn, :index)
end
end
```
We'll save a discussion of `use HelloWeb, :controller` for the [Controllers guide](controllers.html). For now, let's focus on the `index` action.
All controller actions take two arguments. The first is `conn`, a struct which holds a ton of data about the request. The second is `params`, which are the request parameters. Here, we are not using `params`, and we avoid compiler warnings by prefixing it with `_`.
The core of this action is `render(conn, :index)`. It tells Phoenix to render the `index` template. The modules responsible for rendering are called views. By default, Phoenix views are named after the controller (`HelloController`) and format (`HTML` in this case), so Phoenix is expecting a `HelloWeb.HelloHTML` module to exist and define an `index/1` function.
### A new view
Phoenix views act as the presentation layer. For example, we expect the output of rendering `index` to be a complete HTML page. To make our lives easier, we often use templates for creating those HTML pages.
Let's create a new view. Create `lib/hello_web/controllers/hello_html.ex` and make it look like this:
```elixir
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
end
```
To add templates to this view, we can define them as functions in the module or in separate files.
Let's start by defining a function:
```elixir
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
def index(assigns) do
~H"""
Hello!
"""
end
end
```
We defined a function that receives `assigns` as arguments and used [the `~H` sigil](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2) to specify the content we want to render. Inside the `~H` sigil, we used a templating language called HEEx, which stands for "HTML+EEx". `EEx` is a library for embedding Elixir that ships as part of Elixir itself. "HTML+EEx" is a Phoenix extension of EEx that is HTML aware, with support for HTML validation, components, and automatic escaping of values. The latter protects you from security vulnerabilities like Cross-Site Scripting with no extra work on your part. We say that any function that receives `assigns` and returns templates to be a **function component**.
A template file works in the same way. Let's give it a try by defining a template in its own file. First, delete our `def index(assigns)` function from above and replace it with an `embed_templates` declaration:
```elixir
defmodule HelloWeb.HelloHTML do
use HelloWeb, :html
embed_templates "hello_html/*"
end
```
Here we are saying we want to embed all `.heex` templates found in the sibling `hello_html` directory into our module as function components.
Next, we need to add files to the `lib/hello_web/controllers/hello_html` directory. A template file has the following structure: `NAME.FORMAT.TEMPLATING_LANGUAGE`. In our case, let's create an `index.html.heex` file at `lib/hello_web/controllers/hello_html/index.html.heex`:
```heex
Hello World, from Phoenix!
```
Phoenix will see the template file and compile it into an `index(assigns)` function, similar as before. There is no runtime or performance difference between the two styles.
Also note the controller name (`HelloController`) and the view name (`HelloHTML`) and their respective files, `hello_controller.ex` and `hello_html.ex` all follow the same naming convention, and are named after each other. You could name the directory anything you want, as long as you update the `embed_templates` setting accordingly, but it's best to follow conventions.
```console
lib/hello_web
├── controllers
│ ├── hello_controller.ex
│ ├── hello_html.ex
│ ├── hello_html
| ├── index.html.heex (NEW FILE!)
```
Now that we've got the route, controller, view, and template, we should be able to point our browser at [http://localhost:4000/hello](http://localhost:4000/hello) and see our greeting from Phoenix!
In case you stopped the server along the way, the task to restart it is `mix phx.server`. If you didn't stop it, everything should update on the fly: Phoenix has hot code reloading!
## Layouts
Even though our `index.html.heex` file consists of only a single `section` tag, the page we get is a full HTML document. Our index template is actually rendered into a separate layout: `lib/hello_web/components/layouts/root.html.heex`, which contains the basic HTML skeleton of the page. If you open this file, you'll see a line that looks like this at the bottom:
```heex
{@inner_content}
```
This line injects our template into the layout before the HTML is sent off to the browser. The root layout, as the name implies, is a barebone layout with mostly the `` tag and a structure that is shared across **all of your pages**. Richer features, such as sidebar, menus, etc. are part of your application layout, which we will explore in a couple sections below.
## From endpoint to views
Having built our first page, we're beginning to understand how the request life-cycle is put together. Now let's take a more holistic look at it.
All HTTP requests start in our application endpoint. You can find it as a module named `HelloWeb.Endpoint` in `lib/hello_web/endpoint.ex`. Once you open up the endpoint file, you will see that, similar to the router, the endpoint has many calls to `plug`. `Plug` is a library and a specification for stitching web applications together. It is an essential part of how Phoenix handles requests and we will discuss it in detail in the [Plug guide](plug.html) coming next.
For now, it suffices to say that each plug defines a slice of request processing. In the endpoint you will find a skeleton roughly like this:
```elixir
defmodule HelloWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :hello
plug Plug.Static, ...
plug Plug.RequestId
plug Plug.Telemetry, ...
plug Plug.Parsers, ...
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, ...
plug HelloWeb.Router
end
```
Each of these plugs have a specific responsibility that we will learn later. The last plug is precisely the `HelloWeb.Router` module. This allows the endpoint to delegate all further request processing to the router. As we now know, its main responsibility is to map verb/path pairs to controllers. The controller then tells a view to render a template.
At this moment, you may be thinking this can be a lot of steps to simply render a page. However, as our application grows in complexity, we will see that each layer serves a distinct purpose:
* endpoint (`Phoenix.Endpoint`) - the endpoint contains the common and initial path that all requests go through. If you want something to happen on all requests, it goes in the endpoint.
* router (`Phoenix.Router`) - the router is responsible for dispatching verb/path pairs to controllers. The router also allows us to scope functionality. For example, some pages in your application may require user authentication, others may not.
* controller (`Phoenix.Controller`) - the job of the controller is to retrieve request information, talk to your business domain, and prepare data for the presentation layer.
* view - the view handles the structured data from the controller and converts it to a presentation to be shown to users. Views are often named after the content format they are rendering.
Let's do a quick recap on how the last three components work together by adding another page. This time, we will use some additional features, such as layout components and content interpolation.
## Another new page
Let's add just a little complexity to our application. We're going to add a new page that will recognize a piece of the URL, label it as a "messenger" and pass it through the controller into the template so our messenger can say hello.
As we did last time, the first thing we'll do is create a new route.
### Another new route
For this exercise, we're going to reuse `HelloController` created at the [previous step](request_lifecycle.html#a-new-controller) and add a new `show` action. We'll add a line just below our last route, like this:
```elixir
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
get "/hello", HelloController, :index
get "/hello/:messenger", HelloController, :show
end
```
Notice that we use the `:messenger` syntax in the path. Phoenix will take whatever value that appears in that position in the URL and convert it into a parameter. For example, if we point the browser at: `http://localhost:4000/hello/Frank`, the value of `"messenger"` will be `"Frank"`.
### Another new action
Requests to our new route will be handled by the `HelloWeb.HelloController` `show` action. We already have the controller at `lib/hello_web/controllers/hello_controller.ex`, so all we need to do is edit that controller and add a `show` action to it. This time, we'll need to extract the messenger from the parameters so that we can pass it (the messenger) to the template. To do that, we add this show function to the controller:
```elixir
def show(conn, %{"messenger" => messenger}) do
render(conn, :show, messenger: messenger)
end
```
Within the body of the `show` action, we also pass a third argument to the render function, a key-value pair where `:messenger` is the key, and the `messenger` variable is passed as the value.
If the body of the action needs access to the full map of parameters bound to the `params` variable, in addition to the bound messenger variable, we could define `show/2` like this:
```elixir
def show(conn, %{"messenger" => messenger} = params) do
...
end
```
It's good to remember that the keys of the `params` map will always be strings, and that the equals sign does not represent assignment, but is instead a [pattern match](https://hexdocs.pm/elixir/pattern-matching.html) assertion.
### Another new template
For the last piece of this puzzle, we'll need a new template. Since it is for the `show` action of `HelloController`, it will go into the `lib/hello_web/controllers/hello_html` directory and be called `show.html.heex`. It will look surprisingly like our `index.html.heex` template, except that we will need to display the name of our messenger. Let's write the new template down and then explain what it does:
```heex
Hello World, from {@messenger}!
```
If you point your browser to [http://localhost:4000/hello/Frank](http://localhost:4000/hello/Frank), you should see a page that looks like this:
Let's break what the template does into parts. This template has the `.heex` extension which stands for HTML + Embedded Elixir. There are three features from HEEx we are using in the template above:
* Assigns, such as `@messenger` and `@flash` - values we pass to the view from the controller are collectively called our "assigns". We could access our messenger value via `assigns.messenger` and `assigns.flash`, but Phoenix gives us the much cleaner `@` syntax for use in templates.
* Content interpolation, such as `{@messenger}` - any Elixir code that goes between `{...}` will be executed, and the resulting value will replace the tag in the HTML output
* Function component tags, as in `` - the reason templates are called function components is because we can compose them! In this case, there is an `HelloWeb.Layouts.app` function, that defines our application layout, which we invoke with our custom content
Also note how these three features compose: we are passing the `@flash` assign as an Elixir value to the `` component. As we will learn later, flash messages are used to display temporary messages to the user, such as success or error messages. We will learn more about them in the [Components and HEEx templates](components.html) guide.
We are done! Feel free to play around a bit. Whatever you put after `/hello/` will appear on the page as your messenger.
================================================
FILE: guides/routing.md
================================================
# Routing
> **Requirement**: This guide expects that you have gone through the [introductory guides](installation.html) and got a Phoenix application [up and running](up_and_running.html).
> **Requirement**: This guide expects that you have gone through the [Request life-cycle guide](request_lifecycle.html).
Routers are the main hubs of Phoenix applications. They match HTTP requests to controller actions, wire up real-time channel handlers, and define a series of pipeline transformations scoped to a set of routes.
The router file that Phoenix generates, `lib/hello_web/router.ex`, will look something like this one:
```elixir
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
end
# Other scopes may use custom stacks.
# scope "/api", HelloWeb do
# pipe_through :api
# end
# ...
end
```
Both the router and controller module names will be prefixed with the name you gave your application suffixed with `Web`.
The first line of this module, `use HelloWeb, :router`, simply makes Phoenix router functions available in our particular router.
Scopes have their own section in this guide, so we won't spend time on the `scope "/", HelloWeb do` block here. The `pipe_through :browser` line will get a full treatment in the "Pipelines" section of this guide. For now, you only need to know that pipelines allow a set of plugs to be applied to different sets of routes.
Inside the scope block, however, we have our first actual route:
```elixir
get "/", PageController, :home
```
`get` is a Phoenix macro that corresponds to the HTTP verb GET. Similar macros exist for other HTTP verbs, including POST, PUT, PATCH, DELETE, OPTIONS, CONNECT, TRACE, and HEAD.
> #### Why the macros? {: .info}
>
> Phoenix does its best to keep the usage of macros low. You may have noticed, however, that the `Phoenix.Router` relies heavily on macros. Why is that?
>
> We use `get`, `post`, `put`, and `delete` to define your routes. We use macros for two purposes:
>
> * They define the routing engine, used on every request, to choose which controller to dispatch the request to. Thanks to macros, Phoenix compiles all of your routes to a huge case-statement with pattern matching rules, which is heavily optimized by the Erlang VM
>
> * For each route you define, we also define metadata to implement `Phoenix.VerifiedRoutes`. As we will soon learn, verified routes allow us to reference any route as if it were a plain looking string, except that it is verified by the compiler to be valid (making it much harder to ship broken links, forms, mails, etc to production)
>
> In other words, the router relies on macros to build applications that are faster and safer. Also remember that macros in Elixir are compile-time only, which gives plenty of stability after the code is compiled. As we will learn next, Phoenix also provides introspection for all defined routes via `mix phx.routes`.
## Examining routes
Phoenix provides an excellent tool for investigating routes in an application: `mix phx.routes`.
Let's see how this works. Go to the root of a newly-generated Phoenix application and run `mix phx.routes`. You should see something like the following, generated with all routes you currently have:
```console
$ mix phx.routes
GET / HelloWeb.PageController :home
...
```
The route above tells us that any HTTP GET request for the root of the application will be handled by the `home` action of the `HelloWeb.PageController`.
## Resources
The router supports other macros besides those for HTTP verbs like [`get`](`Phoenix.Router.get/3`), [`post`](`Phoenix.Router.post/3`), and [`put`](`Phoenix.Router.put/3`). The most important among them is [`resources`](`Phoenix.Router.resources/4`). Let's add a resource to our `lib/hello_web/router.ex` file like this:
```elixir
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
resources "/users", UserController
...
end
```
For now it doesn't matter that we don't actually have a `HelloWeb.UserController`.
Run `mix phx.routes` once again at the root of your project. You should see something like the following:
```console
...
GET /users HelloWeb.UserController :index
GET /users/:id/edit HelloWeb.UserController :edit
GET /users/new HelloWeb.UserController :new
GET /users/:id HelloWeb.UserController :show
POST /users HelloWeb.UserController :create
PATCH /users/:id HelloWeb.UserController :update
PUT /users/:id HelloWeb.UserController :update
DELETE /users/:id HelloWeb.UserController :delete
...
```
This is the standard matrix of HTTP verbs, paths, and controller actions. For a while, this was known as RESTful routes, but most consider this a misnomer nowadays. Let's look at them individually.
- A GET request to `/users` will invoke the `index` action to show all the users.
- A GET request to `/users/:id/edit` will invoke the `edit` action with an ID to retrieve an individual user from the data store and present the information in a form for editing.
- A GET request to `/users/new` will invoke the `new` action to present a form for creating a new user.
- A GET request to `/users/:id` will invoke the `show` action with an id to show an individual user identified by that ID.
- A POST request to `/users` will invoke the `create` action to save a new user to the data store.
- A PATCH request to `/users/:id` will invoke the `update` action with an ID to save the updated user to the data store.
- A PUT request to `/users/:id` will also invoke the `update` action with an ID to save the updated user to the data store.
- A DELETE request to `/users/:id` will invoke the `delete` action with an ID to remove the individual user from the data store.
If we don't need all these routes, we can be selective using the `:only` and `:except` options to filter specific actions.
Let's say we have a read-only posts resource. We could define it like this:
```elixir
resources "/posts", PostController, only: [:index, :show]
```
Running `mix phx.routes` shows that we now only have the routes to the index and show actions defined.
```console
GET /posts HelloWeb.PostController :index
GET /posts/:id HelloWeb.PostController :show
```
Similarly, if we have a comments resource, and we don't want to provide a route to delete one, we could define a route like this.
```elixir
resources "/comments", CommentController, except: [:delete]
```
Running `mix phx.routes` now shows that we have all the routes except the DELETE request to the delete action.
```console
GET /comments HelloWeb.CommentController :index
GET /comments/:id/edit HelloWeb.CommentController :edit
GET /comments/new HelloWeb.CommentController :new
GET /comments/:id HelloWeb.CommentController :show
POST /comments HelloWeb.CommentController :create
PATCH /comments/:id HelloWeb.CommentController :update
PUT /comments/:id HelloWeb.CommentController :update
```
The `Phoenix.Router.resources/4` macro describes additional options for customizing resource routes.
## Verified Routes
Phoenix includes `Phoenix.VerifiedRoutes` module which provides compile-time checks of router paths against your router by using the `~p` sigil. For example, you can write paths in controllers, tests, and templates and the compiler will make sure those actually match routes defined in your router.
Let's see it in action. Run `iex -S mix` at the root of the project. We'll define a throwaway example module that builds a couple `~p` route paths.
```elixir
iex> defmodule RouteExample do
...> use HelloWeb, :verified_routes
...>
...> def example do
...> ~p"/comments"
...> ~p"/unknown/123"
...> end
...> end
warning: no route path for HelloWeb.Router matches "/unknown/123"
iex:5: RouteExample.example/0
{:module, RouteExample, ...}
iex>
```
Notice how the first call to an existing route, `~p"/comments"` gave no warning, but a bad route path `~p"/unknown/123"` produced a compiler warning, just as it should. This is significant because it allows us to write otherwise hard-coded paths in our application and the compiler will let us know whenever we write a bad route or change our routing structure.
Phoenix projects are set up out of the box to allow use of verified routes throughout your web layer, including tests. For example in your templates you can render `~p` links:
```heex
Welcome Page!View Comments
```
Or in a controller, issue a redirect:
```elixir
redirect(conn, to: ~p"/comments/#{comment}")
```
Using `~p` for route paths ensures our application paths and URLs stay up to date with the router definitions. The compiler will catch bugs for us, and let us know when we change routes that are referenced elsewhere in our application.
### More on verified routes
What about paths with query strings? You can add query string key values directly, as a keyword list or map of values, for example:
```elixir
~p"/users/17?admin=true&active=false"
"/users/17?admin=true&active=false"
~p"/users/17?#{[admin: true]}"
"/users/17?admin=true"
~p"/users/17?#{%{admin: true}}"
"/users/17?admin=true"
```
What if we need a full URL instead of a path? Just wrap your path with a call to `Phoenix.VerifiedRoutes.url/1`, which is imported everywhere that `~p` is available:
```elixir
url(~p"/users")
"http://localhost:4000/users"
```
The `url` calls will get the host, port, proxy port, and SSL information needed to construct the full URL from the configuration parameters set for each environment. We'll talk about configuration in more detail in its own guide. For now, you can take a look at `config/dev.exs` file in your own project to see those values.
## Nested resources
It is also possible to nest resources in a Phoenix router. Let's say we also have a `posts` resource that has a many-to-one relationship with `users`. That is to say, a user can create many posts, and an individual post belongs to only one user. We can represent that by adding a nested route in `lib/hello_web/router.ex` like this:
```elixir
resources "/users", UserController do
resources "/posts", PostController
end
```
When we run `mix phx.routes` now, in addition to the routes we saw for `users` above, we get the following set of routes:
```console
...
GET /users/:user_id/posts HelloWeb.PostController :index
GET /users/:user_id/posts/:id/edit HelloWeb.PostController :edit
GET /users/:user_id/posts/new HelloWeb.PostController :new
GET /users/:user_id/posts/:id HelloWeb.PostController :show
POST /users/:user_id/posts HelloWeb.PostController :create
PATCH /users/:user_id/posts/:id HelloWeb.PostController :update
PUT /users/:user_id/posts/:id HelloWeb.PostController :update
DELETE /users/:user_id/posts/:id HelloWeb.PostController :delete
...
```
We see that each of these routes scopes the posts to a user ID. For the first one, we will invoke `PostController`'s `index` action, but we will pass in a `user_id`. This implies that we would display all the posts for that individual user only. The same scoping applies for all these routes.
When building paths for nested routes, we will need to interpolate the IDs where they belong in route definition. For the following `show` route, `42` is the `user_id`, and `17` is the `post_id`.
```elixir
user_id = 42
post_id = 17
~p"/users/#{user_id}/posts/#{post_id}"
"/users/42/posts/17"
```
Verified routes also support the `Phoenix.Param` protocol, but we don't need to concern ourselves with Elixir protocols just yet. Just know that once we start building our application with structs like `%User{}` and `%Post{}`, we'll be able to interpolate those data structures directly into our `~p` paths and Phoenix will pluck out the correct fields to use in the route.
```elixir
~p"/users/#{user}/posts/#{post}"
"/users/42/posts/17"
```
Notice how we didn't need to interpolate `user.id` or `post.id`? This is particularly nice if we decide later we want to make our URLs a little nicer and start using slugs instead. We don't need to change any of our `~p`'s!
## Scoped routes
Scopes are a way to group routes under a common path prefix and scoped set of plugs. We might want to do this for admin functionality, APIs, and especially for versioned APIs. Let's say we have user-generated reviews on a site, and that those reviews first need to be approved by an administrator. The semantics of these resources are quite different, and they might not share the same controller. Scopes enable us to segregate these routes.
The paths to the user-facing reviews would look like a standard resource.
```console
/reviews
/reviews/1234
/reviews/1234/edit
...
```
The administration review paths can be prefixed with `/admin`.
```console
/admin/reviews
/admin/reviews/1234
/admin/reviews/1234/edit
...
```
We accomplish this with a scoped route that sets a path option to `/admin` like this one. We can nest this scope inside another scope, but instead, let's set it by itself at the root, by adding to `lib/hello_web/router.ex` the following:
```elixir
scope "/admin", HelloWeb.Admin do
pipe_through :browser
resources "/reviews", ReviewController
end
```
We define a new scope where all routes are prefixed with `/admin` and all controllers are under the `HelloWeb.Admin` namespace.
Running `mix phx.routes` again, in addition to the previous set of routes we get the following:
```console
...
GET /admin/reviews HelloWeb.Admin.ReviewController :index
GET /admin/reviews/:id/edit HelloWeb.Admin.ReviewController :edit
GET /admin/reviews/new HelloWeb.Admin.ReviewController :new
GET /admin/reviews/:id HelloWeb.Admin.ReviewController :show
POST /admin/reviews HelloWeb.Admin.ReviewController :create
PATCH /admin/reviews/:id HelloWeb.Admin.ReviewController :update
PUT /admin/reviews/:id HelloWeb.Admin.ReviewController :update
DELETE /admin/reviews/:id HelloWeb.Admin.ReviewController :delete
...
```
This looks good, but there is a problem here. Remember that we wanted both user-facing review routes `/reviews` and the admin ones `/admin/reviews`. If we now include the user-facing reviews in our router under the root scope like this:
```elixir
scope "/", HelloWeb do
pipe_through :browser
...
resources "/reviews", ReviewController
end
scope "/admin", HelloWeb.Admin do
pipe_through :browser
resources "/reviews", ReviewController
end
```
and we run `mix phx.routes`, we get output for each scoped route:
```console
...
GET /reviews HelloWeb.ReviewController :index
GET /reviews/:id/edit HelloWeb.ReviewController :edit
GET /reviews/new HelloWeb.ReviewController :new
GET /reviews/:id HelloWeb.ReviewController :show
POST /reviews HelloWeb.ReviewController :create
PATCH /reviews/:id HelloWeb.ReviewController :update
PUT /reviews/:id HelloWeb.ReviewController :update
DELETE /reviews/:id HelloWeb.ReviewController :delete
...
GET /admin/reviews HelloWeb.Admin.ReviewController :index
GET /admin/reviews/:id/edit HelloWeb.Admin.ReviewController :edit
GET /admin/reviews/new HelloWeb.Admin.ReviewController :new
GET /admin/reviews/:id HelloWeb.Admin.ReviewController :show
POST /admin/reviews HelloWeb.Admin.ReviewController :create
PATCH /admin/reviews/:id HelloWeb.Admin.ReviewController :update
PUT /admin/reviews/:id HelloWeb.Admin.ReviewController :update
DELETE /admin/reviews/:id HelloWeb.Admin.ReviewController :delete
```
What if we had a number of resources that were all handled by admins? We could put all of them inside the same scope like this:
```elixir
scope "/admin", HelloWeb.Admin do
pipe_through :browser
resources "/images", ImageController
resources "/reviews", ReviewController
resources "/users", UserController
end
```
Here's what `mix phx.routes` tells us:
```console
...
GET /admin/images HelloWeb.Admin.ImageController :index
GET /admin/images/:id/edit HelloWeb.Admin.ImageController :edit
GET /admin/images/new HelloWeb.Admin.ImageController :new
GET /admin/images/:id HelloWeb.Admin.ImageController :show
POST /admin/images HelloWeb.Admin.ImageController :create
PATCH /admin/images/:id HelloWeb.Admin.ImageController :update
PUT /admin/images/:id HelloWeb.Admin.ImageController :update
DELETE /admin/images/:id HelloWeb.Admin.ImageController :delete
GET /admin/reviews HelloWeb.Admin.ReviewController :index
GET /admin/reviews/:id/edit HelloWeb.Admin.ReviewController :edit
GET /admin/reviews/new HelloWeb.Admin.ReviewController :new
GET /admin/reviews/:id HelloWeb.Admin.ReviewController :show
POST /admin/reviews HelloWeb.Admin.ReviewController :create
PATCH /admin/reviews/:id HelloWeb.Admin.ReviewController :update
PUT /admin/reviews/:id HelloWeb.Admin.ReviewController :update
DELETE /admin/reviews/:id HelloWeb.Admin.ReviewController :delete
GET /admin/users HelloWeb.Admin.UserController :index
GET /admin/users/:id/edit HelloWeb.Admin.UserController :edit
GET /admin/users/new HelloWeb.Admin.UserController :new
GET /admin/users/:id HelloWeb.Admin.UserController :show
POST /admin/users HelloWeb.Admin.UserController :create
PATCH /admin/users/:id HelloWeb.Admin.UserController :update
PUT /admin/users/:id HelloWeb.Admin.UserController :update
DELETE /admin/users/:id HelloWeb.Admin.UserController :delete
```
This is great, exactly what we want. Note how every route and controller is properly namespaced.
Scopes can also be arbitrarily nested, but you should do it carefully as nesting can sometimes make our code confusing and less clear. With that said, suppose that we had a versioned API with resources defined for images, reviews, and users. Then technically, we could set up routes for the versioned API like this:
```elixir
scope "/api", HelloWeb.Api do
pipe_through :api
scope "/v1", V1 do
resources "/images", ImageController
resources "/reviews", ReviewController
resources "/users", UserController
end
end
```
You can run `mix phx.routes` to see how these definitions will look like.
Interestingly, we can use multiple scopes with the same path as long as we are careful not to duplicate routes. The following router is perfectly fine with two scopes defined for the same path:
```elixir
defmodule HelloWeb.Router do
use Phoenix.Router
...
scope "/", HelloWeb do
pipe_through :browser
resources "/users", UserController
end
scope "/", AnotherAppWeb do
pipe_through :browser
resources "/posts", PostController
end
...
end
```
If we do duplicate a route — which means two routes having the same path — we'll get this familiar warning:
```console
warning: this clause cannot match because a previous clause at line 16 always matches
```
## Pipelines
We have come quite a long way in this guide without talking about one of the first lines we saw in the router: `pipe_through :browser`. It's time to fix that.
Pipelines are a series of plugs that can be attached to specific scopes. If you are not familiar with plugs, we have an [in-depth guide about them](plug.html).
Routes are defined inside scopes and scopes may pipe through multiple pipelines. Once a route matches, Phoenix invokes all plugs defined in all pipelines associated to that route. For example, accessing `/` will pipe through the `:browser` pipeline, consequently invoking all of its plugs.
Phoenix defines two pipelines by default, `:browser` and `:api`, which can be used for a number of common tasks. In turn we can customize them as well as create new pipelines to meet our needs.
### The `:browser` and `:api` pipelines
As their names suggest, the `:browser` pipeline prepares for routes which render requests for a browser, and the `:api` pipeline prepares for routes which produce data for an API.
The `:browser` pipeline has six plugs: The `plug :accepts, ["html"]` defines the accepted request format or formats. `:fetch_session`, which, naturally, fetches the session data and makes it available in the connection. `:fetch_live_flash`, which fetches any flash messages from LiveView and merges them with the controller flash messages. Then, the plug `:put_root_layout` will store the root layout for rendering purposes. Later `:protect_from_forgery` and `:put_secure_browser_headers`, protects form posts from cross-site forgery.
Currently, the `:api` pipeline only defines `plug :accepts, ["json"]`.
The router invokes a pipeline on a route defined within a scope. Routes outside of a scope have no pipelines. Although the use of nested scopes is discouraged (see above the versioned API example), if we call `pipe_through` within a nested scope, the router will invoke all `pipe_through`'s from parent scopes, followed by the nested one.
Those are a lot of words bunched up together. Let's take a look at some examples to untangle their meaning.
Here's another look at the router from a newly generated Phoenix application, this time with the `/api` scope uncommented back in and a route added.
```elixir
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", HelloWeb do
pipe_through :browser
get "/", PageController, :home
end
# Other scopes may use custom stacks.
scope "/api", HelloWeb do
pipe_through :api
resources "/reviews", ReviewController
end
# ...
end
```
When the server accepts a request, the request will always first pass through the plugs in our endpoint, after which it will attempt to match on the path and HTTP verb.
Let's say that the request matches our first route: a GET to `/`. The router will first pipe that request through the `:browser` pipeline - which will fetch the session data, fetch the flash, and execute forgery protection - before it dispatches the request to `PageController`'s `home` action.
Conversely, suppose the request matches any of the routes defined by the [`resources/2`](`Phoenix.Router.resources/2`) macro. In that case, the router will pipe it through the `:api` pipeline — which currently only performs content negotiation — before it dispatches further to the correct action of the `HelloWeb.ReviewController`.
If no route matches, no pipeline is invoked and a 404 error is raised.
### Creating new pipelines
Phoenix allows us to create our own custom pipelines anywhere in the router. To do so, we call the [`pipeline/2`](`Phoenix.Router.pipeline/2`) macro with these arguments: an atom for the name of our new pipeline and a block with all the plugs we want in it.
```elixir
defmodule HelloWeb.Router do
use HelloWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {HelloWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :auth do
plug HelloWeb.Authentication
end
scope "/reviews", HelloWeb do
pipe_through [:browser, :auth]
resources "/", ReviewController
end
end
```
The above assumes there is a plug called `HelloWeb.Authentication` that performs authentication and is now part of the `:auth` pipeline.
Note that pipelines themselves are plugs, so we can plug a pipeline inside another pipeline. For example, we could rewrite the `auth` pipeline above to automatically invoke `browser`, simplifying the downstream pipeline call:
```elixir
pipeline :auth do
plug :browser
plug :ensure_authenticated_user
plug :ensure_user_owns_review
end
scope "/reviews", HelloWeb do
pipe_through :auth
resources "/", ReviewController
end
```
## How to organize my routes?
In Phoenix, we tend to define several pipelines, that provide specific functionality. For example, the `:browser` and `:api` pipelines are meant to be accessed by specific clients, browsers and http clients respectively.
Perhaps more importantly, it is also very common to define pipelines specific to authentication and authorization. For example, you might have a pipeline that requires all users are authenticated. Another pipeline may enforce only admin users can access certain routes.
Once your pipelines are defined, you reuse the pipelines in the desired scopes, grouping your routes around their pipelines. For example, going back to our reviews example. Let's say anyone can read a review, but only authenticated users can create them. Your routes could look like this:
```elixir
pipeline :browser do
...
end
pipeline :auth do
plug HelloWeb.Authentication
end
scope "/" do
pipe_through [:browser]
get "/reviews", PostController, :index
get "/reviews/:id", PostController, :show
end
scope "/" do
pipe_through [:browser, :auth]
get "/reviews/new", PostController, :new
post "/reviews", PostController, :create
end
```
Note in the above how the routes are split across different scopes. While the separation can be confusing at first, it has one big upside: it is very easy to inspect your routes and see all routes that, for example, require authentication and which ones do not. This helps with auditing and making sure your routes have the proper scope.
You can create as few or as many scopes as you want. Because pipelines are reusable across scopes, they help encapsulate common functionality and you can compose them as necessary on each scope you define.
## Forward
The `Phoenix.Router.forward/4` macro can be used to send all requests that start with a particular path to a particular plug. Let's say we have a part of our system that is responsible (it could even be a separate application or library) for running jobs in the background, it could have its own web interface for checking the status of the jobs. We can forward to this admin interface using:
```elixir
defmodule HelloWeb.Router do
use HelloWeb, :router
...
scope "/", HelloWeb do
...
end
forward "/jobs", BackgroundJob.Plug
end
```
This means that all routes starting with `/jobs` will be sent to the `BackgroundJob.Plug` module. Inside the plug, you can match on subroutes, such as `/pending` and `/active` that shows the status of certain jobs.
We can even mix the [`forward/4`](`Phoenix.Router.forward/4`) macro with pipelines. If we wanted to ensure that the user was authenticated and was an administrator in order to see the jobs page, we could use the following in our router.
```elixir
defmodule HelloWeb.Router do
use HelloWeb, :router
...
scope "/" do
pipe_through [:authenticate_user, :ensure_admin]
forward "/jobs", BackgroundJob.Plug
end
end
```
This means the plugs in the `authenticate_user` and `ensure_admin` pipelines will be called before the `BackgroundJob.Plug` allowing them to send an appropriate response and halt the request accordingly.
`BackgroundJob.Plug` can be implemented as any "Module Plug" discussed in the [Plug guide](plug.html). Note though it is not advised to forward to another Phoenix endpoint. This is because plugs defined by your app and the forwarded endpoint would be invoked twice, which may lead to errors.
## Summary
Routing is a big topic, and we have covered a lot of ground here. The important points to take away from this guide are:
- Routes which begin with an HTTP verb name expand to a single clause of the match function.
- Routes declared with `resources` expand to 8 clauses of the match function.
- Resources may restrict the number of match function clauses by using the `only:` or `except:` options.
- Any of these routes may be nested.
- Any of these routes may be scoped to a given path.
- Using verified routes with `~p` for compile-time route checks
================================================
FILE: guides/security.md
================================================
# Security
All software exposed to the public internet will be attacked. Most Phoenix applications fall into this category, so it is useful to understand common types of web application security vulnerabilities, the impact of each, and how to avoid them. Consider the following scenario:
You are responsible for a banking application which handles transactions via a Phoenix backend. A new code change introduces a remote code execution (RCE) vulnerability, granting an attacker production SSH access to the server. This results in the database being exfiltrated, user accounts being compromised, and customer funds being stolen.
With the rise of generative AI coding tools, proper judgement on the security of code has become more important than ever. This document will detail how severe vulnerabilities can occur in a Phoenix project and secure coding best practices to help avoid an incident.
## Remote Code Execution (RCE)
A remote code execution (RCE) vulnerability grants an attacker the equivalent of production SSH access to your web server. This type of vulnerability is often considered the worst possible case, because it does not require user interaction and allows an attacker to bypass all security controls in your application.
You should never pass untrusted user input to the following functions:
```
# Code functions
Code.eval_string/3
Code.eval_file/2
Code.eval_quoted/3
# EEX functions
eval_string/3
eval_file/3
# Command injection functions
:os.cmd/2
System.cmd/3
System.shell/2
```
All of these functions execute arbitrary code on your server if passed external input. The risk here is obvious to most programmers, so it is rare to find a Phoenix application vulnerable in this way.
The more common and often unexpected way a Phoenix application is vulnerable to RCE is via the Erlang function `binary_to_term`. From [the Erlang docs:](https://www.erlang.org/doc/apps/erts/erlang.html#binary_to_term/2)
> When decoding binaries from untrusted sources, the untrusted source may submit data in a way to create resources, such as atoms and remote references, that cannot be garbage collected and lead to a Denial of Service (DoS) attack. In such cases, use `binary_to_term/2` with the `safe` option.
This warning is confusing, because the `safe` option mentioned above only prevents the creation of new atoms at runtime. It does not prevent the creation of executable terms via malicious user input, which is a much greater risk.
```
# Not safe
:erlang.binary_to_term(user_input, [:safe])
# Safe
Plug.Crypto.non_executable_binary_to_term(user_input, [:safe])
```
The function `Plug.Crypto.non_executable_binary_to_term` prevents the creation of executable terms at runtime. If you are curious how this vulnerability can occur in the real world, see the writeup on [CVE-2020-15150 in the library Paginator.](https://www.alphabot.com/security/blog/2020/elixir/Remote-code-execution-vulnerability-in-Elixir-based-Paginator-project.html)
## SQL Injection
SQL injection is on par with RCE as a highly severe vulnerability that leads to your entire database being leaked, and can even be leveraged to achieve code execution in some contexts. The good news for Phoenix developers is the majority of applications use Ecto, which is the interface to a dedicated database, typically PostgreSQL or MySQL. The Ecto query syntax protects against SQL injection by default:
```
# Safe, using query syntax
def a_get_fruit(min_q) do
from(
f in Fruit,
where:
f.quantity >= ^min_q and
f.secret == false
)
|> Repo.all()
end
```
The `fragment` function seems to be a vector for SQL injection:
```
# Fails to compile
def b_get_fruit(min_q) do
from(
f in Fruit,
where: fragment("f0.quantity >= #{min_q} AND f0.secret = FALSE")
)
|> Repo.all()
end
```
Yet if you try to compile the above code it fails:
```text
Compiling 1 file (.ex)
== Compilation error in file lib/basket/goods.ex ==
** (Ecto.Query.CompileError) to prevent SQL injection attacks, fragment(...)
does not allow strings to be interpolated as the first argument via the `^`
operator, got: `"f0.quantity >= #{min_q} AND f0.secret = FALSE"`
```
What about passing arguments via fragment?
```
# Safe
def c_get_fruit(min_q) do
min_q = String.to_integer(min_q)
from(
f in Fruit,
where: fragment("f0.quantity >= ? AND f0.secret = FALSE", ^min_q)
)
|> Repo.all()
end
```
The above code is safe because the external user input is safely parameterized into the query. What if you decide to pass arguments directly into raw SQL?
```
# Safe
def d_get_fruit(min_q) do
q = """
SELECT f.id, f.name, f.quantity, f.secret
FROM fruits AS f
WHERE f.quantity > $1 AND f.secret = FALSE
"""
{:ok, %{rows: rows}} =
Ecto.Adapters.SQL.query(Repo, q, [String.to_integer(min_q)])
end
```
The above code is safe, similar to the fragment example, because the user input is being safely parameterized.
Constructing a SQL query string via user input, then passing it directly to the query function does lead to SQL injection:
```
# Vulnerable to SQL injection
def e_get_fruit(min_q) do
q = """
SELECT f.id, f.name, f.quantity, f.secret
FROM fruits AS f
WHERE f.quantity > #{min_q} AND f.secret = FALSE
"""
{:ok, %{rows: rows}} =
Ecto.Adapters.SQL.query(Repo, q)
end
```
If you find yourself writing raw SQL, take care not to interpolate directly into the string, but rather use parameters for external input into the query.
## Server Side Request Forgery (SSRF)
Server Side Request Forgery (SSRF) is a critical vulnerability that has been the root cause of major data breaches. The problem is untrusted user input being used to make outbound HTTP requests, which leads to the exploitation of services reachable from your Phoenix application.
When you create a server in most cloud providers today, for example AWS EC2, there will be a [metadata service](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html) exposed on a private IP address. In the case of AWS it's on `169.254.169.254`. If you send an HTTP request to:
`http://169.254.169.254/iam/security-credentials`
From the server, it will return credentials for the AWS account. If you write a web application (for example in Phoenix) where a user can enter a URL, and then view the response of an HTTP request sent to that URL, that functionality is potentially vulnerable to SSRF. For a real world incident see [the Capital One breach.](https://dl.acm.org/doi/pdf/10.1145/3546068)
Your reaction to this may be "This seems like an unsafe feature, considering they named an entire vulnerability class after it." You are not alone in this opinion, AWS introduced some SSRF specific mitigations in [2019 via IMDSv2](https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/), requiring HTTP requests to the metadata endpoint to have session authentication.
The instance metadata service of cloud servers is not the only target for SSRF: services such as PostgreSQL, MySQL, Redis, Elasticsearch, even custom micro-services that you believe are not exposed to the public internet may be vulnerable to SSRF. Consider an internal server with vulnerabilities that can be exploited via HTTP requests, not directly exposed to the public internet. Yet if that network also has a Phoenix application vulnerable to SSRF, an attacker can go through the Phoenix server to attack the private server.
To summarize, using an HTTP client does not necessarily mean your application is vulnerable to SSRF. Rather the following conditions must be met:
1. You are using external user input to construct URLs
2. An attacker is able to send HTTP requests to some vulnerable server on your network
For an Elixir specific example consider the Req library:
```
req = Req.new(base_url: "https://api.github.com")
Req.get!(req, url: "/repos/sneako/finch").body["description"]
#=> "Elixir HTTP client, focused on performance"
```
It is possible to override the `base_url` value:
```
req = Req.new(base_url: "https://elixir-lang.org/")
user_input = "https://dashbit.co/blog"
Req.get!(req, url: user_input)
```
The above code sends a request to `https://dashbit.co/blog`, NOT `https://elixir-lang.org/`.
Consider a similar example in Tesla:
```
plug Tesla.Middleware.BaseUrl, "https://example.com/foo"
MyClient.get("http://example.com/bar") # equals to GET http://example.com/bar
```
Even if you are setting a base URL in your application, don't treat it as a security barrier, because as these examples show it can be overwritten. Take care to avoid sending HTTP requests based on user input if you can avoid it, and be mindful of services on the same network as your Phoenix application that could be exploited via SSRF.
## Cross Origin Resource Sharing (CORS) Misconfiguration
Cross Origin Resource Sharing (CORS) is a feature in modern web browsers which can be used to bypass the same origin policy, which is useful because your application can now load resources from a different site in the user's web browser. This is normally restricted by default.
Consider a Phoenix API with a single page application (SPA) on a different domain:
`app.example.com` - React frontend
`api.example.com` - Backend in Elixir/Phoenix
The frontend `(app.example.com)` needs to fetch information about the current user, however the origins of these projects are different. The user's web browser will block the request unless CORS is enabled between the sites.
Setting the following in the Phoenix application:
```
plug CORSPlug, origin: ["https://app.example.com"]
```
Is the correct way to do this. However it is also possible to set a policy that is far too broad, for example:
```
plug CORSPlug, origin: ~r/^http.*/
```
This is a major security risk, because now a malicious site loaded by a user who is logged into the application can read sensitive data, for example API keys. Take care to ensure only trusted sites are allowed here.
## Broken Access Control
In Phoenix controllers the standard way to handle authorization is putting the current user in the assigns, and then doing:
```
user = conn.assigns.current_user
```
Many projects add this as a third argument in the controller actions:
```
def action(conn, _) do
args = [conn, conn.params, conn.assigns.current_user]
apply(__MODULE__, action_name(conn), args)
end
```
This is the correct approach. The wrong approach is to accept arbitrary user input when making authorization decisions, for example:
```
# Not safe
def index(conn, %{"user" => user_email})
user = Accounts.get_user_by_email(user_email)
```
In the above example an attacker can simply change the submitted `user_email` string to an arbitrary value to perform an action as a different user. Using `conn.assigns.current_user` avoids this problem.
Related to the above concept, the design of Ecto in Phoenix takes the risk of mass assignment into consideration, because you have to explicitly define what parameters are allowed to be set from user supplied data. Consider a simple users schema:
```
schema "users" do
field :email, :string
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :confirmed_at, :naive_datetime
field :is_admin, :boolean
timestamps(type: :utc_datetime)
end
def registration_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email, :password, :is_admin])
|> validate_email()
|> validate_password(opts)
end
```
Assume that the corresponding signup form is exposed to the public internet. Can you spot the vulnerability? The problem is that `:is_admin` should never be set via external user input. Anyone on the public internet can now create a user where `:is_admin` is set to true in the database, which is likely not the intent of the developer.
## Cross Site Scripting (XSS)
By default, user input in Phoenix is escaped so that:
```
<%= "" %>
```
is shown in your browser as:
```text
<hello>
```
Note that this looks like a normal string of `` from an end user perspective, the `<` and `>` are only visible if you inspect the page with your browser tools.
It is possible to bypass this protection via the `raw/1` function. For example, consider the string `hello`.
```
# This will render the literal string hello
<%= "hello" %>
# This will render a bold hello
<%= raw "hello" %>
```
With the ability to inject script tags, an attacker now has the ability to execute JavaScript in a victim's browser, for example by submitting a string such as:
```html
```
If someone submits the above input to your application, and it is displayed to logged in users, you have a serious problem. See the [2005 MySpace worm](https://en.wikipedia.org/wiki/Samy_(computer_worm)) for a real world example of this.
User input should never be passed into the `raw/1` function. There are some additional vectors for XSS in Phoenix applications, for example consider the following controller functions:
```
def html_resp(conn, %{"i" => i}) do
html(conn, "#{i}")
end
```
Because the HTML is being generated from user input, the function is vulnerable to XSS. This can also happen with `put_resp_content_type`:
```
def send_resp_html(conn, %{"i" => i}) do
conn
|> put_resp_content_type("text/html")
|> send_resp(200, "#{i}")
end
```
File uploads can also lead to XSS.
```
def new_upload(conn, _params) do
render(conn, "new_upload.html")
end
def upload(conn, %{"upload" => upload}) do
%Plug.Upload{content_type: content_type, filename: filename, path: path} = upload
ImgServer.put(filename, %{content_type: content_type, bin: File.read!(path)})
redirect(conn, to: Routes.page_path(conn, :view_photo, filename))
end
def view_photo(conn, %{"filename" => filename}) do
case ImgServer.get(filename) do
%{content_type: content_type, bin: bin} ->
conn
|> put_resp_content_type(content_type)
|> send_resp(200, bin)
_ ->
conn
|> put_resp_content_type("text/html")
|> send_resp(404, "Not Found")
end
end
```
User input determines the `content-type` of the file. There is no validation on the type of file being uploaded, meaning `content-type` can be set so an HTML page is rendered. This is the source of the vulnerability. Consider the file `xss.html`, with the contents:
```html
```
This will result in JavaScript being executed in the browser of the victim who views the image. Restricting the `put_resp_content_type` argument to only image files would fix this vulnerability.
## Cross Site Request Forgery (CSRF)
### Standard CSRF in HTML forms
Cross site request forgery (CSRF) is a vulnerability that exists due to a quirk in how web browsers work. Consider a social media website that is vulnerable to CSRF. An attacker creates a malicious website aimed at legitimate users. When a victim visits the malicious site, it triggers a POST request in the victim’s browser, sending a message that was written by the attacker. This results in the victim’s account making a post written by the attacker.
But why is this possible? Shouldn't the web browser block POST requests initiated by a different website? There is a cookie feature called SameSite that addresses this problem, however it's good to understand what CSRF is and why Phoenix has built in protections.
Consider the following form:
```html
```
This maps to the following HTTP request:
```http_request_and_response
POST /posts HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 53
post[title]=My+Title&post[body]=This+is+the+body
```
An attacker can embed the following form on `attacker.com`:
```html
```
Note that this form does not even have to be visible to the victim user. When the user visits `attacker.com` the form will automatically submit a POST request on behalf of the victim, with the victim's current session cookie, to the vulnerable site.
The way most web frameworks, including Phoenix, mitigate this vulnerability is by requiring a CSRF token when submitting a form.
```html
```
This changes the previous HTTP request to:
```http_request_and_response
POST /posts HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 53
post[title]=My+Title&post[body]=This+is+the+body&post[_csrf_token]=WUZXJh07BhAIJ24jP1d-KQEpLwYmMDwQ0-2eYNLH_x8oHoO_qv_HJDqZ
```
Because the token is randomly generated, an attacker cannot predict what the value will be. The application is safe against CSRF attacks because Phoenix checks the value of this token by default via the `:protect_from_forgery` plug, which is included in the default `:browser` pipeline of a new Phoenix project:
```
# router.ex
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {CarafeWeb.LayoutView, :root}
plug :protect_from_forgery # <-- Checks the CSRF token value
plug :put_secure_browser_headers
plug :fetch_current_user
end
```
CSRF protections are included in Phoenix by default, so your application is most likely not vulnerable if you are using the default settings.
### Action Re-Use CSRF
Most descriptions of CSRF focus on state changing POST requests and the need for random tokens, as was just covered. Is CSRF possible with a GET request? Yes, GET requests should never be used to perform state changing actions in a web application (place an order, transfer money, etc) because they cannot be protected against CSRF in the same way POST requests can.
Consider the following form:
```heex
<.form :let={f} for={@bio_changeset} action={~p"/users/settings/edit_bio"} method="post" id="edit_bio">
"""
end
@doc """
Renders a button with navigation support.
## Examples
<.button>Send!
<.button phx-click="go" variant="primary">Send!
<.button navigate={~p"/"}>Home
"""
attr :rest, :global, include: ~w(href navigate patch method download name value disabled)
attr :class, :any
attr :variant, :string, values: ~w(primary)
slot :inner_block, required: true
def button(%{rest: rest} = assigns) do
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
assigns =
assign_new(assigns, :class, fn ->
["btn", Map.fetch!(variants, assigns[:variant])]
end)
if rest[:href] || rest[:navigate] || rest[:patch] do
~H"""
<.link class={@class} {@rest}>
{render_slot(@inner_block)}
"""
else
~H"""
"""
end
end
@doc """
Renders an input with label and error messages.
A `Phoenix.HTML.FormField` may be passed as argument,
which is used to retrieve the input name, id, and values.
Otherwise all attributes may be passed explicitly.
## Types
This function accepts all HTML input types, considering that:
* You may also set `type="select"` to render a `