phoenixframework / phoenix Goto Github PK
View Code? Open in Web Editor NEWPeace of mind from prototype to production
Home Page: https://www.phoenixframework.org
License: MIT License
Peace of mind from prototype to production
Home Page: https://www.phoenixframework.org
License: MIT License
Greetings all,
I've put together a functioning view layer that I'm pleased with and would like feedback on the approach and conventions before it lands in master.
/cc @josevalim, @ericmj, @darkofabijan, @nurugger07
branch: https://github.com/phoenixframework/phoenix/tree/cm/templates
Here's a rundown of how things work:
views
├── Layouts
│ ├── application.html.eex
│ └── layouts.ex - optional, implicity created if missing
├── Profiles
│ ├── profiles.ex - optional, implicity created if missing
│ └── show.html.eex
├── Users
│ ├── nav
│ │ └── top.html.eex
│ ├── show.html.eex
│ └── users.ex - optional, implicity created if missing
└── views.ex
Phoenix makes a distinction between what Templates and Views are. Put simply, Views render Templates.
lib/my_app/views/views.ex
)All subviews use MyApp.Views
, allowing the base view to serve as
an area to import/define common view functionality (helpers, aliases, etc)
defmodule MyApp.Views do
use Phoenix.View.Base
defmacro __using__(_) do
quote do
use Phoenix.View
alias MyApp.Views
import unquote(__MODULE__)
end
end
def default_title, do: "Welcome"
...
end
use MyApp.Views
to have base functionality definedSubview modules are created automatically in the event the user simply wants to render templates using only functions exposed from the base view. At compile time, Phoenix will check for the existence of a subview when it sees a camel-cased directory within the lib/my_app/views
folder. If the corresponding subview elixir module does not exist, a skeleton module is compiled automatically.
lib/my_app/views/Users/users.ex
)defmodule MyApp.Views.Users do
use MyApp.Views
def truncate_bio(bio, length), do: String.slice(bio, 0, length) <> "..."
...
end
Views.Users.render("show.html", name: "chris")
Views.Profiles.render("show.html", homepage_url: "example.com")
# render a view within another view for layout support,
# exposes `@inner` assign to parent view
Views.Users.render("show.html",
name: "chris",
within: {Views.Layouts, "application.html"}
)
safe
is required right now when rendering inline templates. I may rework to safe nested renders automatically:
<%= render("nav/top.html", []) |> safe %>
<div>
User <%= @name %>
</div>
<html>
<title><%= @title || default_title %></title>
<%= @inner %>
</html>
The render/3
macro expands at compile time to conventionally call a View module whose name mirrors the the Controller's name. The template extension is determined from the request content-type. i.e.
defmodule MyApp.Controllers.Users do
use Phoenix.Controller
def show(conn) do
render conn, "show", name: "José"
end
end
Expands at compile time to:
def show(conn) do
render_view conn, MyApp.Views.Users, MyApp.Views.Layouts, "show", name: "José"
end
# For an html request at runtime, `render_view` would invoke:
MyApp.Views.Users.render("show.html",
name: "José",
within: {MyApp.Views.Layouts, "application.html"}
)
The current implementation can also be easily extended to support other tempting engines. For example, calliope integration would only require a small shim that compiled the templates to conform to Phoenix's precompiled naming conventions.
That's it. There's still work to do, but everything here is implemented. I plan to have an initial release sometime this June. Thanks!
Currently I'm just dropping in Dynamo's cookie module. Seems like a common enough need for web applications that Phoenix should support this OOTB.
I just did a
brew update
brew install erlang --devel # to get r17
brew install elixir # to get 0.13.0
git clone https://github.com/phoenixframework/phoenix.git && cd phoenix && mix do deps.get, compile
and the compile stage died with
* Compiling ranch
Could not find rebar, which is needed to build ranch
I can install a local copy which is just used by mix
Shall I install this local copy? [Yn]
** (Mix) Could not access url http://elixir-lang.org/rebar, error: :no_scheme
Am I missing something? I'm very new to elixir so I might be doing something silly. I also don't know what other info you need.
Thanks!
RIght now I'm not sure we have a way to halt connections, we can send a response but that doesn't stop phoenix from running the rest of the commands or plugs.
I'm a bit lost here; I'd like to access the raw request body of a POST/PUT/PATCH request, and can't figure out how to do it. I had a look at the Plug library, but it seems to require one to use a Plug.Parser, which in turn restricts the request to use either a urlencoded or web form content type.
A POST action in the examples would be nice, to see how I could use phoenix for an API service.
Example:
resources "comments", Controllers.Comments do
get "/special", Controllers.Comments, :special # it works without leading slash
end
The matching path that we generate is comments/:comment_id//special
. Solution should be just a bit smarter path join function.
Also throughout router tests and in examples we always specify single route path without leading slash. It's different from Rails. We can make a switch and require leading slash or just support both. Leading slash makes it somehow more clear that first param is path.
In my example application, I'm able to get websockets working just fine. However, there's an error report:
=ERROR REPORT==== 12-Feb-2014::01:17:26 ===
Error in process <0.229.0> with exit value: {[{reason,{'Elixir.RuntimeError','__exception__',<<2319 bytes>>}},{mfa,{'Elixir.Plug.Adapters.Cowboy.Handler',init,3}},{stacktrace,[{'Elixir.Plug.Adapters.Cowboy.Handler',init,3,[{file,"lib/plug/adapters/cowboy/handler.ex"},{line,13}]},...
Elixir.T3WebsocketServer.Router: get: ["favicon.ico"]
=ERROR REPORT==== 12-Feb-2014::01:17:26 ===
Ranch listener 'Elixir.T3WebsocketServer.Router.HTTP' had connection process started with cowboy_protocol:start_link/4 at <0.229.0> exit with reason: {[{reason,{'Elixir.RuntimeError','__exception__',<<"Cowboy adapter expected T3WebsocketServer.Router to return Plug.Conn but got: {:ok, Plug.Conn[adapter: {Plug.Adapters.Cowboy.Connection, {:http_req, #Port<0.4969>, :ranch_tcp, :keepalive, #PID<0.229.0>, \"GET\", :\"HTTP/1.1\", {{127, 0, 0, 1}, 64201}, \"localhost\", :undefined, 3030, \"/pages\", :undefined, \"\", :undefined, [], [{\"host\", \"localhost:3030\"}, {\"connection\", \"keep-alive\"}, {\"cache-control\", \"max-age=0\"}, {\"accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"}, {\"user-agent\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31\"}, {\"accept-encoding\", \"gzip,deflate,sdch\"}, {\"accept-language\", \"en-US,en;q=0.8\"}, {\"accept-charset\", \"ISO-8859-1,utf-8;q=0.7,*;q=0.3\"}, {\"cookie\", \"jirafe.metric.boxes=%7B%22revenue%22%3A%7B%7D%2C%22orders%22%3A%7B%7D%2C%22visits%22%3A%7B%7D%2C%22conv_rate%22%3A%7B%7D%2C%22aov%22%3A%7B%7D%2C%22rpv%22%3A%7B%7D%7D; jirafe.active.site_id=68472; _pk_id.68472.1fff=3961f189ff30bb0b.1369279691.6.1372000564.1371725123.\"}], [{\"connection\", [\"keep-alive\"]}], :undefined, [], :waiting, \"\", :undefined, false, :done, [], \"\", :undefined}}, assigns: [], cookies: Plug.Connection.Unfetched[aspect: :cookies], host: \"localhost\", method: \"GET\", params: [], path_info: [\"pages\"], port: 3030, query_string: \"\", req_cookies: Plug.Connection.Unfetched[aspect: :cookies], req_headers: [{\"host\", \"localhost:3030\"}, {\"connection\", \"keep-alive\"}, {\"cache-control\", \"max-age=0\"}, {\"accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\"}, {\"user-agent\", \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31\"}, {\"accept-encoding\", \"gzip,deflate,sdch\"}, {\"accept-language\", \"en-US,en;q=0.8\"}, {\"accept-charset\", \"ISO-8859-1,utf-8;q=0.7,*;q=0.3\"}, {\"cookie\", \"jirafe.metric.boxes=%7B%22revenue%22%3A%7B%7D%2C%22orders%22%3A%7B%7D%2C%22visits%22%3A%7B%7D%2C%22conv_rate%22%3A%7B%7D%2C%22aov%22%3A%7B%7D%2C%22rpv%22%3A%7B%7D%7D; jirafe.active.site_id=68472; _pk_id.68472.1fff=3961f189ff30bb0b.1369279691.6.1372000564.1371725123.\"}], resp_body: nil, resp_cookies: [], resp_headers: [{\"cache-control\", \"max-age=0, private, must-revalidate\"}, {\"content-type\", \"text/html; charset=utf-8\"}], scheme: :http, state: :sent, status: 200]}">>}},{mfa,{'Elixir.Plug.Adapters.Cowboy.Handler',init,3}},{stacktrace,[{'Elixir.Plug.Adapters.Cowboy.Handler',init,3,[{file,"lib/plug/adapters/cowboy/handler.ex"},{line,13}]},{cowboy_handler,handler_init,4,[{file,"src/cowboy_handler.erl"},{line,69}]},{cowboy_protocol,execute,4,[{file,"src/cowboy_protocol.erl"},{line,471}]}]},{req,[{socket,#Port<0.4969>},{transport,ranch_tcp},{connection,keepalive},{pid,<0.229.0>},{method,<<"GET">>},{version,'HTTP/1.1'},{peer,{{127,0,0,1},64201}},{host,<<"localhost">>},{host_info,undefined},{port,3030},{path,<<"/pages">>},{path_info,undefined},{qs,<<>>},{qs_vals,undefined},{bindings,[]},{headers,[{<<"host">>,<<"localhost:3030">>},{<<"connection">>,<<"keep-alive">>},{<<"cache-control">>,<<"max-age=0">>},{<<"accept">>,<<"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8">>},{<<"user-agent">>,<<"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31">>},{<<"accept-encoding">>,<<"gzip,deflate,sdch">>},{<<"accept-language">>,<<"en-US,en;q=0.8">>},{<<"accept-charset">>,<<"ISO-8859-1,utf-8;q=0.7,*;q=0.3">>},{<<"cookie">>,<<"jirafe.metric.boxes=%7B%22revenue%22%3A%7B%7D%2C%22orders%22%3A%7B%7D%2C%22visits%22%3A%7B%7D%2C%22conv_rate%22%3A%7B%7D%2C%22aov%22%3A%7B%7D%2C%22rpv%22%3A%7B%7D%7D; jirafe.active.site_id=68472; _pk_id.68472.1fff=3961f189ff30bb0b.1369279691.6.1372000564.1371725123.">>}]},{p_headers,[{<<"connection">>,[<<"keep-alive">>]}]},{cookies,undefined},{meta,[]},{body_state,waiting},{buffer,<<>>},{multipart,undefined},{resp_compress,false},{resp_state,waiting},{resp_headers,[]},{resp_body,<<>>},{onresponse,undefined}]},{opts,{'Elixir.T3WebsocketServer.Router',[]}}],[{cowboy_protocol,execute,4,[{file,"src/cowboy_protocol.erl"},{line,471}]}]}
When ssl is true Plug https should be used /cc @scrogson
ie, :ok = Plug.Adapters.Cowboy.shutdown(__MODULE__.HTTP)
I went through the installation instructions and ran into the following issue when trying to fetch dependencies + compiling:
☁ phoenix [master] mix do deps.get, compile
** (SyntaxError) nofile:1: invalid token: %{"cowboy": {:git, "git://github.com/extend/cowboy.git", "05024529679d1d0203b8dcd6e2932cc2a526d370", []},
(elixir) src/elixir.erl:113: :elixir.eval/3
(elixir) lib/code.ex:130: Code.do_eval_string/3
(mix) lib/mix/deps/lock.ex:58: Mix.Deps.Lock.read/0
(mix) lib/mix/tasks/deps.get.ex:22: Mix.Tasks.Deps.Get.run/1
(elixir) lib/enum.ex:517: Enum."-each/2-lists^foreach/1-0-"/2
(elixir) lib/enum.ex:517: Enum.each/2
(mix) lib/mix/cli.ex:59: Mix.CLI.run_task/2
Installed elixir 0.12.5 via homebrew
Current master of elixir, phoenix, freshest erlang:
mix phoenix.start
String.from_char_list!/1 is deprecated, please use String.from_char_data!/1 instead
(elixir) lib/string.ex:1204: String.from_char_list!/1
lib/ex_conf/utils.ex:5: ExConf.Utils.capitalize/1
lib/ex_conf/config.ex:96: ExConf.Config.conf_module_for_env/2
lib/phoenix/plugs/code_reloader.ex:7: Phoenix.Plugs.CodeReloader.call/2
lib/foo/router.ex:1: anonymous fn/1 in Foo.Router.call/2
lib/phoenix/plugs/error_handler.ex:8: Phoenix.Plugs.ErrorHandler.wrap/3
lib/plug/adapters/cowboy/handler.ex:9: Plug.Adapters.Cowboy.Handler.init/3
Two things would be nice
curl
executable script to get and compile phoenixThoughts?
Can't compile plug. This is the error message:
== Compilation error on file lib/plug/mime.ex == ** (CompileError) lib/plug/mime.ex:51: undefined function lc/2 (elixir) src/elixir.erl:189: :elixir.quoted_to_erl/3 (stdlib) erl_eval.erl:657: :erl_eval.do_apply/6 (elixir) src/elixir.erl:157: :elixir.erl_eval/2
could not compile dependency plug, mix compile failed. You can recompile this dependency with mix deps.compile plug or update it with mix deps.update plug
did mix deps.update plug
and mix deps.update plug
but same error message appear.
Did on Elixir 0.13.1 & 0.13.3-dev with same result
I suggest adding the possibility of overriding the default error and not found handlers.
Currently not_found, error and error_with_trace are implemented in Phoenix.Controller. The problem is that when executing the catch all match or in the error handling plug we don't have the actual controller in the scope, only the router.
I think one way to implement this would be to move these functions to Phoenix.Router and allow them to be overridden in the app router using behaviours.
What do you think?
Trying to setup Elixir/Pheonix for the first time, having ran the following commands
brew install erlang --devel
brew install elixir
git clone https://github.com/phoenixframework/phoenix.git && cd phoenix && mix do deps.get, compile
The error that I get is:
== Compilation error on file lib/plug/adapters/cowboy/conn.ex ==
** (ArgumentError) cannot access module File.Stat because it is not a record
(elixir) expanding macro: Kernel.access/2
lib/plug/adapters/cowboy/conn.ex:33: Plug.Adapters.Cowboy.Conn.send_file/4
(elixir) src/elixir.erl:157: :elixir.erl_eval/2
(elixir) src/elixir.erl:150: :elixir.eval_forms/4
(elixir) src/elixir_lexical.erl:17: :elixir_lexical.run/2
(elixir) lib/kernel/parallel_compiler.ex:91: anonymous fn/3 in Kernel.ParallelCompiler.spawn_compilers/8
could not compile dependency plug, mix compile failed. You can recompile this dependency with `mix deps.compile plug` or update it with `mix deps.update plug`
I've tried both suggested commands and the same error occurs on compiling plug.
Running Elixir 0.13.2
against erlang: stable R16B03-1 (bottled), devel 17.0, HEAD
Very new to Elixir so any pointing in the right direction would be greatly appreciated.
What do you think about following changes to routing DSL?
Currently DSL looks like this:
defmodule Router do
use Phoenix.Router
get "users/:id", UsersController, :show, as: :user
get "profiles/profile-:id", UsersController, :show
get "users/top", UsersController, :top, as: :top
get "route_that_crashes", UsersController, :crash
get "files/:user_name/*path", FilesController, :show
get "backups/*path", FilesController, :show
get "static/images/icons/*image", FilesController, :show
resources "comments", CommentsController
get "users/:user_id/comments/:id", CommentsController, :show
end
With changes:
defmodule Router do
use Phoenix.Router
get "users/:id", to: "users#show", as: :user
get "profiles/profile-:id", to: "users#show"
get "users/top", to: "users#top", as: :top
get "route_that_crashes", to: "users#crash"
get "files/:user_name/*path", to: "files#show"
get "backups/*path", to: "files#show"
get "static/images/icons/*image", to: "files#show"
resources :comments
get "users/:user_id/comments/:id", to: "comments#show"
end
Implementation would be pretty simple.
I have detected that we are missing this while testing mix phoenix routes
on example phoenix application.
Output is:
GET posts/:id Posts#show
GET posts/new Posts#new
GET posts Posts#index
POST posts Posts#create
PUT posts/:id Posts#update
PATCH posts/:id Posts#update
DELETE posts/:id Posts#destroy
GET posts/:post_id/comments/:id Comments#show
GET posts/:post_id/comments/new Comments#new
GET posts/:post_id/comments Comments#index
POST posts/:post_id/comments Comments#create
PUT posts/:post_id/comments/:id Comments#update
PATCH posts/:post_id/comments/:id Comments#update
DELETE posts/:post_id/comments/:id Comments#destroy
While it should be something like this:
posts GET posts Posts#index
POST posts Posts#create
new_post GET posts/new Posts#new
post GET posts/:id Posts#show
PUT posts/:id Posts#update
PATCH posts/:id Posts#update
DELETE posts/:id Posts#destroy
post_comments GET posts/:post_id/comments Comments#index
POST posts/:post_id/comments Comments#create
new_post_comment GET posts/:post_id/comments/new Comments#new
post_comment GET posts/:post_id/comments/:id Comments#show
PUT posts/:post_id/comments/:id Comments#update
PATCH posts/:post_id/comments/:id Comments#update
DELETE posts/:post_id/comments/:id Comments#destroy
To get this nice output resources routes should be also reordered. Obviously edit
is missing but we have issue for that #19.
Template engine wise, EEx has no great means for xss protection, but we don't have many other options yet. I'm hoping to include @nurugger07's haml implementation once it's ready (https://github.com/nurugger07/calliope). If we can get xss protection in EEx, I'd be perfectly happy with that as the default template engine, but José has said as implemented today with just string concat that it can't easily be done.
In my example project, if I do not define init/1
, it blows up with:
=INFO REPORT==== 12-Feb-2014::01:14:48 ===
application: t3_websocket_server
exited: {{shutdown,
{failed_to_start_child,'Elixir.T3WebsocketServer.Router',
{'EXIT',
{undef,
[{'Elixir.T3WebsocketServer.Router',init,[[]],[]},
{'Elixir.Plug.Adapters.Cowboy',run,4,
[{file,"lib/plug/adapters/cowboy.ex"},
{line,105}]},
{supervisor,do_start_child,2,
[{file,"supervisor.erl"},{line,308}]},
{supervisor,start_children,3,
[{file,"supervisor.erl"},{line,291}]},
{supervisor,init_children,2,
[{file,"supervisor.erl"},{line,257}]},
{gen_server,init_it,6,
[{file,"gen_server.erl"},{line,304}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,239}]}]}}}},
{'Elixir.T3WebsocketServer',start,[normal,[]]}}
type: temporary
** (Mix) Could not start application t3_websocket_server: {{:shutdown, {:failed_to_start_child, T3WebsocketServer.Router, {:EXIT, {:undef, [{T3WebsocketServer.Router, :init, [[]], []}, {Plug.Adapters.Cowboy, :run, 4, [file: 'lib/plug/adapters/cowboy.ex', line: 105]}, {:supervisor, :do_start_child, 2, [file: 'supervisor.erl', line: 308]}, {:supervisor, :start_children, 3, [file: 'supervisor.erl', line: 291]}, {:supervisor, :init_children, 2, [file: 'supervisor.erl', line: 257]}, {:gen_server, :init_it, 6, [file: 'gen_server.erl', line: 304]}, {:proc_lib, :init_p_do_apply, 3, [file: 'proc_lib.erl', line: 239]}]}}}}, {T3WebsocketServer, :start, [:normal, []]}}
@darkofabijan Looks like ConsoleFormatterTest isn't running since it needs to be an .exs instead of ex. I also noticed that the printed Controllers are prefixed by "Elixir.". I think it's safe for use to drop the Elixir atom prefix.
The implementation of Phoenix.Router.Path.split will not allow using dashes in the value of a named parameter.
For example, this route:
get "/resource/:id", App.Controllers.Resource, :show
will not match http://localhost:4000/resource/75f6306d-a090-46f9-8b80-80fd57ec9a41
But if we define a route that will match http://localhost:4000/resource?id=75f6306d-a090-46f9-8b80-80fd57ec9a41
we'll have the UUID with dashes included available in conn.params["id"]
Maybe I'm missing something, but I think the two cases above should yield similar results and dashes should not be considered a URL separator in the split function.
defmodule Router do
use Phoenix.Router, port: 4000
resources "admin/users", Phoenix.Controllers.Users do
resources "comments", Phoenix.Controllers.Comments
end
end
mix phoenix.routes Router
admin/users GET admin/users Users#index
edit_admin/user GET admin/users/:id/edit Users#edit
admin/user GET admin/users/:id Users#show
new_admin/user GET admin/users/new Users#new
We need to detect these scenarios and convert to underscores
Hey All!
I'm really excited to unveil my plans for the websocket/pubsub layer I've been envisioning for Phoenix. I have an initial working implementation and I'm looking for direct feedback on the api, naming/concepts, and conventions before I merge with master. Here's what we have today:
phoenix.js
depA single websocket connection from the client is shared for all subscribed channels. This is required since browsers limit simultaneous websocket connections. A light javascript library provides socket connection, channel subscription, event binding, etc.
:pg2
I don't have hard benchmarks yet, but pg2
seemed ideal to built pubsub on top of since we get process groups distributed on the mesh for free. This comes with the caveat of potential overhead of nodes being locked for consistency, but I'd like to see how this scales out. Initially stress tests are very promising.
Channels can be thought of as namespaces for "topics" to be subscribed to, as well as a place to group common events and behavior. You can almost conceptually think of them as a typical controller action, except they handle events from clients, and broadcast events to other channels.
Topics are simply a string for clients to subscribe/broadcast about on a particular channel. This could be anything from the string "lobby" on a "rooms" channel, or a particular user id, for a "profiles" channel.
Let's write a bare-bones chat application using these ideas:
rooms
Channel to broadcast on:defmodule Chat.Router do
use Phoenix.Router
use Phoenix.Router.Socket, mount: "/ws"
plug Plug.Static, at: "/static", from: :chat
get "/", Chat.Controllers.Pages, :index, as: :page
channel "rooms", Chat.Channels.Rooms
end
defmodule Chat.Channels.Rooms do
use Phoenix.Channel
@doc """
Authorize socket to join for sub/pub events on this channel & topic
Possible Return Values
{:ok, socket} to authorize sub for channel for requested topic
{:error, socket, reason} to deny sub/pub on this channel
for the requested topic
"""
def join(socket, message) do
reply socket, "join", status: "connected"
broadcast socket, "user:entered", username: "anonymous"
{:ok, socket}
end
def event("new:message", socket, message) do
broadcast socket, "new:message", message
{:ok, socket}
end
end
<h1>Phoenix Chat Example</h1>
<h3>Status: <small id="status">Not Connected</small></h3>
<div id="messages"></div>
<input id="message-input" type="text">
<script type="text/javascript">
$(function(){
var socket = new Phoenix.Socket("ws://" + location.host + "/ws");
var $status = $("#status");
var $messages = $("#messages");
var $input = $("#message-input");
socket.join("rooms", "lobby", {}, function(chan){
$input.off("keypress").on("keypress", function(e) {
if (e.keyCode == 13) {
chan.send("new:message", {body: $input.val()});
$input.val("");
}
});
chan.on("join", function(message){ $status.text("joined"); });
chan.on("new:message", function(message){
$messages.append("<br/>" + message.body);
});
chan.on("user:entered", function(msg){
$messages.append("<br/><i>[" + msg.username + " entered]</i>");
});
});
});
</script>
The neat thing about channels is we can broadcast to them from anywhere in our application. Consider the chat example with active clients in the "rooms" channel, "lobby" topic. We could push a message to the browser directly from iex:
iex> Phoenix.Channel.broadcast("rooms", "lobby", "new:message", body: "hello!")
:ok
This opens up all kinds of realtime updates from any parts of the application to subscribed clients.
That's it! I believe this is flexible enough to accommodate most use-cases and extensible enough for us to iterate on for more advanced features.
Here's the chat example packaged as a little app:
https://github.com/chrismccord/phoenix_chat_example
cm-channels branch:
https://github.com/phoenixframework/phoenix/tree/cm-channels
Big thanks to @jeregrine for doing the legwork on the websocket handler and @HashNuke for helping me flesh out the channel/topic concepts.
Let me know how it looks!
I set up a github org and will be moving the code over soon. So don't be alarmed if your push fails and you need to update your remote refs. The org will help give the project a face and allow easier contributor management. I also have phoenixframework.org where I'd like to see eventually see docs, getting started guides, etc.
We should be able to limit actions for parent resource.
Example:
resources "files", Controllers.Files, only: [:index] do
resources "comments", Controllers.Comments, :except [:destroy]
end
Currently it's throwing following exception:
** (ArgumentError) argument error
(stdlib) :lists.keyfind(:do, 1, {{:., [line: 66], [Kernel, :access]}, [line: 66], [:except, [:destroy]]})
(elixir) lib/keyword.ex:120: Keyword.get/3
(phoenix) lib/phoenix/router/mapper.ex:155: Phoenix.Router.Mapper."MACRO-resources"/4
(phoenix) /home/darko/phoenix/test/phoenix/router/nested_routing_test.exs:66: Phoenix.Router.Mapper.resources/3
/home/darko/phoenix/test/phoenix/router/nested_routing_test.exs:66: Phoenix.Router.NestedTest.Router (module)
I can imagine this working in two ways (or both)
Application.Behaviour
or just let the mix run router
handle thingsHandling configuration is up for discussion. Currently, the only portion of the application that needs configuration support is the Router, but we need to think ahead on how we'd like to handle Phoenix configuration in general. I'd like to frame configuration within the context of a non-monolith setup. We have a couple options off the top of my head:
defmodule Router do
use Phoenix.Router, port: 4000
def conf, do: Conf
resources "users", Users
...
end
defmodule Conf do
def domain(:prod), do: "example.com"
def domain(:dev), do: "example.dev"
def enforce_ssl?(:prod), do: true
def enforce_ssl?(_), do: false
end
This is just a brain dump of ideas. I'd love to hear what everyone thinks and other solutions not mentioned. My current preference is code based configuration, but this rules out nesting, unless we write a Config lib that sprinkles in some macro facilities.
Seem to have trouble compiling phoenix via mix.deps.compile
:
== Compilation error on file lib/phoenix/examples/router.ex ==
could not compile dependency phoenix, mix compile failed. You can recompile this dependency with mix deps.compile phoenix
or update it with mix deps.update phoenix
** (CompileError) deps/phoenix/lib/phoenix/examples/router.ex:2: module Plug.Builder is not loaded and could not be found
Currently the scope helper path prefixes the entire helper function name. I'm accustomed to the Rails resource action prefixing the rest of the helper method, i.e. edit_admin_user_path
. We could deviate here with our own convention of the current behavior, but I prefer the action prefixing the helper as it's easy to grep.
Current behavior:
scope path: "admin", alias: Admin, helper: "admin" do
resources "users", Users
end
mix phoenix.routes Router
admin_users GET admin/users Elixir.Admin.Users#index
admin_edit_user GET admin/users/:id/edit Elixir.Admin.Users#edit
admin_user GET admin/users/:id Elixir.Admin.Users#show
admin_new_user GET admin/users/new Elixir.Admin.Users#new
POST admin/users Elixir.Admin.Users#create
PUT admin/users/:id Elixir.Admin.Users#update
PATCH admin/users/:id Elixir.Admin.Users#update
DELETE admin/users/:id Elixir.Admin.Users#destroy
Hey,
when calling conn.params I would expect a Dict of the parsed Url params. But no matter what I do I get an empty Dict.
Also I can't seem to find the phoenix test case for Http Post with params.
I supposed you rely on the correctness of the Plug.Parsers.URLENCODED but somewhere my params are lost.
Has anyone experienced that before? If not I have to keep digging.
$ http POST http://localhost:4000/alarms alarm=foo --form --verbose
POST /alarms HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, compress
Content-Length: 9
Content-Type: application/x-www-form-urlencoded; charset=utf-8
Host: localhost:4000
User-Agent: HTTPie/0.7.2
alarm=foo
pry(1)> conn
Plug.Conn[adapter: {Plug.Adapters.Cowboy.Connection,
{:http_req, #Port<0.7906>, :ranch_tcp, :keepalive, #PID<0.283.0>, "POST",
:"HTTP/1.1", {{127, 0, 0, 1}, 52806}, "localhost", :undefined, 4000,
"/alarms", :undefined, "", :undefined, [],
[{"host", "localhost:4000"}, {"content-length", "9"},
{"content-type", "application/x-www-form-urlencoded; charset=utf-8"},
{"accept-encoding", "gzip, deflate, compress"}, {"accept", "*/*"},
{"user-agent", "HTTPie/0.7.2"}], [], :undefined, [], :waiting, "alarm=foo",
:undefined, false, :waiting, [], "", :undefined}}, assigns: [],
before_send: [], cookies: Plug.Connection.Unfetched[aspect: :cookies],
host: "localhost", method: "POST", params: [], path_info: ["alarms"],
port: 4000, private: [], query_string: "",
req_cookies: Plug.Connection.Unfetched[aspect: :cookies],
req_headers: [{"host", "localhost:4000"}, {"content-length", "9"},
{"content-type", "application/x-www-form-urlencoded; charset=utf-8"},
{"accept-encoding", "gzip, deflate, compress"}, {"accept", "*/*"},
{"user-agent", "HTTPie/0.7.2"}], resp_body: nil, resp_cookies: [],
resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}],
scheme: :http, state: :unset, status: nil]
pry(2)> conn.params
[]
Has this not been done yet? There are links in the README, but it goes nowhere.
It could be run with mix phoenix.routes
and would print:
GET /events MyApp.Controllers.Events#index
GET /events/new MyApp.Controllers.Events#new
POST /events MyApp.Controllers.Events#create
GET /events/:id MyApp.Controllers.Events#show
GET /events/:id/edit MyApp.Controllers.Events#edit
PUT /events/:id MyApp.Controllers.Events#update
DELETE /events/:id MyApp.Controllers.Events#destroy
With following route specified in routes file it is not routing correctly:
get "/", Blog.Controllers.Pages, :index, as: :page
It works with:
get "", Blog.Controllers.Pages, :index, as: :page
@chrismccord knows about this but I am just documenting it here so we don't forget.
prod should not display a stack trace
I would like to create ex_docs site for Phoenix. It would work pretty much the same way as it works for Elixir. Make script would assume that ex_doc is installed in ../ex_doc
and it would for generate docs for master (for now) and store in the ../docs
. I guess we could host it through GitHub pages. @chrismccord can you please create docs
repository?
I got inspiration for this while writing docs for routes scope macro and seeing that we could improve in this area. It's easier to evaluate docs through generated html docs.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.