Basic Phoenix Concepts
A blank Rails app costs 170 MB while a Phoenix app costs 70 MB.
action_fallback
Arity is the number of arguments that a function takes.
function/1
Elixir operators always return a float.
hd
head
hd(array-name) #returns the first element or head
_
_whatever #tells Elixir that `whatever` can be null
tuple {}
map %{}
Pipe Operator
|>
passes output of earlier function into the next function due to the functional or flowing nature of Elixir. This requires the functional programmer to have more intelligence than an object-oriented programmer as to remember the long chain of cause and effect
puts the result in the last series
Pattern Matching Rules
=
is a ‘match operator’ not an equals sign. This requires more intelligence from the programmer as to decide whether two sides match or not, instead of being equal to just one side like in object-oriented or procedural programming.
- accepts left and right ‘operands’, with left being dominant
- if left is a letter/s, the right is bound to that letter/s-variable
x = 1
x = 'a'
'b' = x # error because left must be a letter/s or number such as 1 + 1 = 2
b = b # error because right side needs non-variable
- if right is a letter/s, the right is matched to the value of the left
x = y
- entering new match operators
x = 2
will re-bind the operands - pin operator
^
prevents re-binding so it prevents the operand does not change
Integer Separator
1123_456_789 --> 123456789
Booleans
Strict:
and
or
not
Non-strict:
&&
||
!
Comparison
>
<
>=
<=
Non-Strict Comparison
!=
==
Strict Comparison
!==
===
String literals
\
escape character indicates a special string with certain abilities
\n
new line eg:
"Donald\nTrump"
#{}
interpolation inserts expression within the string
"Donald Trump #{'J' == 'r' == '.'}"
Atom literal
used as labels or tags
:text
:"<>"
true
Maps
Structs: Specialized Maps
Defining
defmodule ModuleName do
defstruct [:key1, :key2]
end
Calling
%ModuleName{key1: value, key2: value}
module = %ModuleName{key1: value, key2: value}
module.key1
data type must match! (atoms vs strings)
Recursion
Iteration
Conditional Macros for the Lazy
->
if-then
case condition do
true -> a
false -> b
end
case mega_function(input) do
{:error, error_message} -> {:error, error_message}
{:ok, mega_function_output} -> case mini_function(input) do
{:error, error_message} -> {:error, error_message}
{:ok, input} -> %{key: mini_function_output}
end
end
with <-
if-then, if-then, if-then
case mega_function(input) do
with {:ok, mini_function_output} <- mini_function(input)} do
{:ok, %{key: mini_function_output}
# mega_function_ouput has mini_function_output inside of it
end
end
Guard Clauses
Filters functions
def function_name(input) when condition1 do
end
def function_name(input) when condition2 do
end
Functions
enclosed in Modules
def function_name(argument_name) do
def function_name(argument_name\\ "default-value-if-no-argument-name-is-given") do
Anonymous Functions
Setting
function_name = fn(argument1, argument2, argument3..) -> argument1 + argument2 + argument3.. end
eg:
square = fn(x) -> x * x end
function_name = fn(a,b,c,d) -> (a,b,c,d)
function_name = &(&1 + &2 + &3 + &4)
function_name = &(ModuleName.another_function/4)
Calling
function_name.(4) #anonymous function
function_name(4) #named function
Capture Operator: Shortcut for those lazy to write code
&1
is the shortcut for the arguments in fn(x..) -> x..
&1 #represents or captures the first argument
&2 #represents or captures the second argument
eg:
function_name = &(&1 * &2)
&(Module.function_name/1) #shortcut for the whole Module
var_too_lazy_to_type_module = &(LongNamedModule.long_named_function/100)
Erlang Functions
:timer
IO.puts "whatever"
Module
container for the functions
defmodule ModuleName1 do
def function_name(argument1, argument2,..) do
ModuleName2.function_name()
Alias and Import: References to modules
alias Module # just identifies which Module to look into eventually so you call by Module.method()
import Module # actually gets all the functions of Module so you just call method()
@constant = value # assigns a value as a constant at compile-time
Plug: Manipulates data in conn structs
It’s a Module that has a ‘Conn’ Struct. It takes and returns that conn struct between modules.
It allows state management when combined with Agents and Genservers (like Live View). The state is held by the struct and then is passed between Modules. The ability to pass data turns Plugs into Elixir webservers
It has two types:
- Module Plug
- Function Plug
Assigns
assign(key: "#{data.attrib}")
Function Plug
This is a simple function that receives a conn struct, manipulates it, and outputs the modified conn struct
Defining non_piped
and piped
Function Plugs
defmodule App.ModuleName do
import Plug.Conn
def non_pipe(conn, options) do
assign(conn, :non_pipe, 123)
end
def piped(conn, _options) do
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "From a Piped Function Plug that outputted this data inside this conn struct")
end
end
Calling in Elixir
plug :non_piped
Calling in Phoenix View
<%= @conn.assigns[:non_piped] %>
<%= @non_piped %>
Module Plug
This is a large function, as a Module, that needs to be initialized with additional data such as state (i.e. initial state). This makes it more complicated than a function plug
Defining
defmodule App.Modulename do
import Plug.Conn
def init(options) do
options
end
def call(conn, _options) do
end
end
Calling in Elixir
alias ModulePlugName
plug ModulePlugName, [option: "Blah"] when action is [:index]
Calling
Controller
alias Appname.Plugname
plug
function_name.(1,2,3,4)
Function Plug: Adds a Conn to a Module Function
Calling
defmodule App.TotalController do
import App.ModuleName
plug :get_total
..
end
Map
Defining
map = %{:atom => "string", "string" => "string"}
mapshortcut = %{atom: "string", atom2: "string"}
# binding to variable
%{"string" => string}
Calling
map.atom
map["string"]
map[:atom]
Examples:
map = %{ph -> manila, us -> wash}
OTP Processes and State Management
This allows functions to hold data independent of the function which is really held through the Genserver. This makes the OTP a real app.
Processes
- runs the actual functions and modules in memory
- has PID
Tasks: Start-Stops a Function or Module on a Process
- Runs a single process such as polling
- 1 task : 1 process
- has no state
Task.start(function_name)
Agents: Adds Lightweight State management on a Process
Only gets and updates single states. This makes it faster.
Genservers: Adds Heavy Duty State management on a Process
Gets, manipulates, and updates states by allowing “messages”.
Ecto
Handles external data such as databases and JSON APIs
- Repo
- Query
- Schema
- Changset
Repo: communicates with external data source or database
Has common methods:
- get(): gets the record
- insert(): creates the record in that database
- update()
- delete()
- transaction() …
Phoenix uses Repo through:
config.exs
as config:appname, Appname.Repo,
…repo.ex
in/lib/appname/repo.ex
Common methods
Repo.count()
Repo.update_all("tablename", set: [updated_at: Ecto.DateTime.utc])
Schema
Reusable data-model for moving data around. Schemas are a useful shortcut used in querying so you don’t have to specify all the attributes
Schema Associations
sets relationships between tables
- has_many
- has_one
- belongs_to
- many_to_many
Virtual Attributes
Disposable attributes that are not saved to the database. For example, an office address that is used to get a latitude and longitude to store in the database, without storing the office address
Embedded Schema
A schema that doesn’t use a datbase
Changeset
Allows manipulation of the data to match Schema so that it can be passed to the Repo
- Cast: filters the parameters with the attributes to be accepted into the changeset
- Validate: validates those filtered attributes. This includes constraints which are set on the database level
Defining
def changeset(schemaname, attrs) do
schemaname
|> cast(attrs, [:key1, :key2,..])
|> validate_required([:key1])
|> validate_inclusion(:name, ["John", "Jack"])
|> validate_exclusion(:name, ["Jub Jub"])
|> validate_length(:country, is: 2)
|> validate_length(:password, min: 8, max: 32)
end
Calling
attrib_name = get_field(changeset, :attrib_name)
Schemaless Changeset
Defining
model = %{key1: :string, key2: string}
attributes = %{key1: "value", key2: "value"}
changeset = {%{}, model}
|> cast(attributes, Map.keys(model))
|> validate_required([:key1])
end
Ecto Queries (see other article)
Phoenix
Phoenix Ecto
allows changeset shortcuts into forms