1336 lines
211 KiB
HTML
1336 lines
211 KiB
HTML
|
<?xml version="1.0" encoding="utf-8"?>
|
||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
|
||
|
<head>
|
||
|
<!-- 2024-05-13 lun 00:21 -->
|
||
|
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
|
<title>Duckbrain - Manual de inicio</title>
|
||
|
<meta name="author" content="KJ" />
|
||
|
<meta name="generator" content="Org Mode" />
|
||
|
<style>
|
||
|
#content { max-width: 60em; margin: auto; }
|
||
|
.title { text-align: center;
|
||
|
margin-bottom: .2em; }
|
||
|
.subtitle { text-align: center;
|
||
|
font-size: medium;
|
||
|
font-weight: bold;
|
||
|
margin-top:0; }
|
||
|
.todo { font-family: monospace; color: red; }
|
||
|
.done { font-family: monospace; color: green; }
|
||
|
.priority { font-family: monospace; color: orange; }
|
||
|
.tag { background-color: #eee; font-family: monospace;
|
||
|
padding: 2px; font-size: 80%; font-weight: normal; }
|
||
|
.timestamp { color: #bebebe; }
|
||
|
.timestamp-kwd { color: #5f9ea0; }
|
||
|
.org-right { margin-left: auto; margin-right: 0px; text-align: right; }
|
||
|
.org-left { margin-left: 0px; margin-right: auto; text-align: left; }
|
||
|
.org-center { margin-left: auto; margin-right: auto; text-align: center; }
|
||
|
.underline { text-decoration: underline; }
|
||
|
#postamble p, #preamble p { font-size: 90%; margin: .2em; }
|
||
|
p.verse { margin-left: 3%; }
|
||
|
pre {
|
||
|
border: 1px solid #e6e6e6;
|
||
|
border-radius: 3px;
|
||
|
background-color: #f2f2f2;
|
||
|
padding: 8pt;
|
||
|
font-family: monospace;
|
||
|
overflow: auto;
|
||
|
margin: 1.2em;
|
||
|
}
|
||
|
pre.src {
|
||
|
position: relative;
|
||
|
overflow: auto;
|
||
|
}
|
||
|
pre.src:before {
|
||
|
display: none;
|
||
|
position: absolute;
|
||
|
top: -8px;
|
||
|
right: 12px;
|
||
|
padding: 3px;
|
||
|
color: #555;
|
||
|
background-color: #f2f2f299;
|
||
|
}
|
||
|
pre.src:hover:before { display: inline; margin-top: 14px;}
|
||
|
/* Languages per Org manual */
|
||
|
pre.src-asymptote:before { content: 'Asymptote'; }
|
||
|
pre.src-awk:before { content: 'Awk'; }
|
||
|
pre.src-authinfo::before { content: 'Authinfo'; }
|
||
|
pre.src-C:before { content: 'C'; }
|
||
|
/* pre.src-C++ doesn't work in CSS */
|
||
|
pre.src-clojure:before { content: 'Clojure'; }
|
||
|
pre.src-css:before { content: 'CSS'; }
|
||
|
pre.src-D:before { content: 'D'; }
|
||
|
pre.src-ditaa:before { content: 'ditaa'; }
|
||
|
pre.src-dot:before { content: 'Graphviz'; }
|
||
|
pre.src-calc:before { content: 'Emacs Calc'; }
|
||
|
pre.src-emacs-lisp:before { content: 'Emacs Lisp'; }
|
||
|
pre.src-fortran:before { content: 'Fortran'; }
|
||
|
pre.src-gnuplot:before { content: 'gnuplot'; }
|
||
|
pre.src-haskell:before { content: 'Haskell'; }
|
||
|
pre.src-hledger:before { content: 'hledger'; }
|
||
|
pre.src-java:before { content: 'Java'; }
|
||
|
pre.src-js:before { content: 'Javascript'; }
|
||
|
pre.src-latex:before { content: 'LaTeX'; }
|
||
|
pre.src-ledger:before { content: 'Ledger'; }
|
||
|
pre.src-lisp:before { content: 'Lisp'; }
|
||
|
pre.src-lilypond:before { content: 'Lilypond'; }
|
||
|
pre.src-lua:before { content: 'Lua'; }
|
||
|
pre.src-matlab:before { content: 'MATLAB'; }
|
||
|
pre.src-mscgen:before { content: 'Mscgen'; }
|
||
|
pre.src-ocaml:before { content: 'Objective Caml'; }
|
||
|
pre.src-octave:before { content: 'Octave'; }
|
||
|
pre.src-org:before { content: 'Org mode'; }
|
||
|
pre.src-oz:before { content: 'OZ'; }
|
||
|
pre.src-plantuml:before { content: 'Plantuml'; }
|
||
|
pre.src-processing:before { content: 'Processing.js'; }
|
||
|
pre.src-python:before { content: 'Python'; }
|
||
|
pre.src-R:before { content: 'R'; }
|
||
|
pre.src-ruby:before { content: 'Ruby'; }
|
||
|
pre.src-sass:before { content: 'Sass'; }
|
||
|
pre.src-scheme:before { content: 'Scheme'; }
|
||
|
pre.src-screen:before { content: 'Gnu Screen'; }
|
||
|
pre.src-sed:before { content: 'Sed'; }
|
||
|
pre.src-sh:before { content: 'shell'; }
|
||
|
pre.src-sql:before { content: 'SQL'; }
|
||
|
pre.src-sqlite:before { content: 'SQLite'; }
|
||
|
/* additional languages in org.el's org-babel-load-languages alist */
|
||
|
pre.src-forth:before { content: 'Forth'; }
|
||
|
pre.src-io:before { content: 'IO'; }
|
||
|
pre.src-J:before { content: 'J'; }
|
||
|
pre.src-makefile:before { content: 'Makefile'; }
|
||
|
pre.src-maxima:before { content: 'Maxima'; }
|
||
|
pre.src-perl:before { content: 'Perl'; }
|
||
|
pre.src-picolisp:before { content: 'Pico Lisp'; }
|
||
|
pre.src-scala:before { content: 'Scala'; }
|
||
|
pre.src-shell:before { content: 'Shell Script'; }
|
||
|
pre.src-ebnf2ps:before { content: 'ebfn2ps'; }
|
||
|
/* additional language identifiers per "defun org-babel-execute"
|
||
|
in ob-*.el */
|
||
|
pre.src-cpp:before { content: 'C++'; }
|
||
|
pre.src-abc:before { content: 'ABC'; }
|
||
|
pre.src-coq:before { content: 'Coq'; }
|
||
|
pre.src-groovy:before { content: 'Groovy'; }
|
||
|
/* additional language identifiers from org-babel-shell-names in
|
||
|
ob-shell.el: ob-shell is the only babel language using a lambda to put
|
||
|
the execution function name together. */
|
||
|
pre.src-bash:before { content: 'bash'; }
|
||
|
pre.src-csh:before { content: 'csh'; }
|
||
|
pre.src-ash:before { content: 'ash'; }
|
||
|
pre.src-dash:before { content: 'dash'; }
|
||
|
pre.src-ksh:before { content: 'ksh'; }
|
||
|
pre.src-mksh:before { content: 'mksh'; }
|
||
|
pre.src-posh:before { content: 'posh'; }
|
||
|
/* Additional Emacs modes also supported by the LaTeX listings package */
|
||
|
pre.src-ada:before { content: 'Ada'; }
|
||
|
pre.src-asm:before { content: 'Assembler'; }
|
||
|
pre.src-caml:before { content: 'Caml'; }
|
||
|
pre.src-delphi:before { content: 'Delphi'; }
|
||
|
pre.src-html:before { content: 'HTML'; }
|
||
|
pre.src-idl:before { content: 'IDL'; }
|
||
|
pre.src-mercury:before { content: 'Mercury'; }
|
||
|
pre.src-metapost:before { content: 'MetaPost'; }
|
||
|
pre.src-modula-2:before { content: 'Modula-2'; }
|
||
|
pre.src-pascal:before { content: 'Pascal'; }
|
||
|
pre.src-ps:before { content: 'PostScript'; }
|
||
|
pre.src-prolog:before { content: 'Prolog'; }
|
||
|
pre.src-simula:before { content: 'Simula'; }
|
||
|
pre.src-tcl:before { content: 'tcl'; }
|
||
|
pre.src-tex:before { content: 'TeX'; }
|
||
|
pre.src-plain-tex:before { content: 'Plain TeX'; }
|
||
|
pre.src-verilog:before { content: 'Verilog'; }
|
||
|
pre.src-vhdl:before { content: 'VHDL'; }
|
||
|
pre.src-xml:before { content: 'XML'; }
|
||
|
pre.src-nxml:before { content: 'XML'; }
|
||
|
/* add a generic configuration mode; LaTeX export needs an additional
|
||
|
(add-to-list 'org-latex-listings-langs '(conf " ")) in .emacs */
|
||
|
pre.src-conf:before { content: 'Configuration File'; }
|
||
|
|
||
|
table { border-collapse:collapse; }
|
||
|
caption.t-above { caption-side: top; }
|
||
|
caption.t-bottom { caption-side: bottom; }
|
||
|
td, th { vertical-align:top; }
|
||
|
th.org-right { text-align: center; }
|
||
|
th.org-left { text-align: center; }
|
||
|
th.org-center { text-align: center; }
|
||
|
td.org-right { text-align: right; }
|
||
|
td.org-left { text-align: left; }
|
||
|
td.org-center { text-align: center; }
|
||
|
dt { font-weight: bold; }
|
||
|
.footpara { display: inline; }
|
||
|
.footdef { margin-bottom: 1em; }
|
||
|
.figure { padding: 1em; }
|
||
|
.figure p { text-align: center; }
|
||
|
.equation-container {
|
||
|
display: table;
|
||
|
text-align: center;
|
||
|
width: 100%;
|
||
|
}
|
||
|
.equation {
|
||
|
vertical-align: middle;
|
||
|
}
|
||
|
.equation-label {
|
||
|
display: table-cell;
|
||
|
text-align: right;
|
||
|
vertical-align: middle;
|
||
|
}
|
||
|
.inlinetask {
|
||
|
padding: 10px;
|
||
|
border: 2px solid gray;
|
||
|
margin: 10px;
|
||
|
background: #ffffcc;
|
||
|
}
|
||
|
#org-div-home-and-up
|
||
|
{ text-align: right; font-size: 70%; white-space: nowrap; }
|
||
|
textarea { overflow-x: auto; }
|
||
|
.linenr { font-size: smaller }
|
||
|
.code-highlighted { background-color: #ffff00; }
|
||
|
.org-info-js_info-navigation { border-style: none; }
|
||
|
#org-info-js_console-label
|
||
|
{ font-size: 10px; font-weight: bold; white-space: nowrap; }
|
||
|
.org-info-js_search-highlight
|
||
|
{ background-color: #ffff00; color: #000000; font-weight: bold; }
|
||
|
.org-svg { }
|
||
|
</style>
|
||
|
<style type="text/css">
|
||
|
.org-bold { /* bold */ font-weight: bold; }.org-bold-italic { /* bold-italic */ font-weight: bold; font-style: italic; }.org-buffer-menu-buffer { /* buffer-menu-buffer */ font-weight: bold; }.org-builtin { /* font-lock-builtin-face */ color: #7a378b; }.org-button { /* button */ text-decoration: underline; }.org-calendar-today { /* calendar-today */ text-decoration: underline; }.org-change-log-acknowledgement { /* change-log-acknowledgement */ color: #b22222; }.org-change-log-conditionals { /* change-log-conditionals */ color: #a0522d; }.org-change-log-date { /* change-log-date */ color: #8b2252; }.org-change-log-email { /* change-log-email */ color: #a0522d; }.org-change-log-file { /* change-log-file */ color: #0000ff; }.org-change-log-function { /* change-log-function */ color: #a0522d; }.org-change-log-list { /* change-log-list */ color: #a020f0; }.org-change-log-name { /* change-log-name */ color: #008b8b; }.org-comint-highlight-input { /* comint-highlight-input */ font-weight: bold; }.org-comint-highlight-prompt { /* comint-highlight-prompt */ color: #00008b; }.org-comment { /* font-lock-comment-face */ color: #999988; font-style: italic; }.org-comment-delimiter { /* font-lock-comment-delimiter-face */ color: #999988; font-style: italic; }.org-completions-annotations { /* completions-annotations */ font-style: italic; }.org-completions-common-part { /* completions-common-part */ color: #000000; background-color: #ffffff; }.org-completions-first-difference { /* completions-first-difference */ font-weight: bold; }.org-constant { /* font-lock-constant-face */ color: #008b8b; }.org-diary { /* diary */ color: #ff0000; }.org-diff-context { /* diff-context */ color: #7f7f7f; }.org-diff-file-header { /* diff-file-header */ background-color: #b3b3b3; font-weight: bold; }.org-diff-function { /* diff-function */ background-color: #cccccc; }.org-diff-header { /* diff-header */ background-color: #cccccc; }.org-diff-hunk-header { /* diff-hunk-header */ background-color: #cccccc; }.org-diff-index { /* diff-index */ background-color: #b3b3b3; font-weight: bold; }.org-diff-nonexistent { /* diff-nonexistent */ background-color: #b3b3b3; font-weight: bold; }.org-diff-refine-change { /* diff-refine-change */ background-color: #d9d9d9; }.org-dired-directory { /* dired-directory */ color: #0000ff; }.org-dired-flagged { /* dired-flagged */ color: #ff0000; font-weight: bold; }.org-dired-header { /* dired-header */ color: #228b22; }.org-dired-ignored { /* dired-ignored */ color: #7f7f7f; }.org-dired-mark { /* dired-mark */ color: #008b8b; }.org-dired-marked { /* dired-marked */ color: #ff0000; font-weight: bold; }.org-dired-perm-write { /* dired-perm-write */ color: #b22222; }.org-dired-symlink { /* dired-symlink */ color: #a020f0; }.org-dired-warning { /* dired-warning */ color: #ff0000; font-weight: bold; }.org-doc { /* font-lock-doc-face */ color: #8b2252; }.org-escape-glyph { /* escape-glyph */ color: #a52a2a; }.org-file-name-shadow { /* file-name-shadow */ color: #7f7f7f; }.org-flyspell-duplicate { /* flyspell-duplicate */ color: #cdad00; font-weight: bold; text-decoration: underline; }.org-flyspell-incorrect { /* flyspell-incorrect */ color: #ff4500; font-weight: bold; text-decoration: underline; }.org-fringe { /* fringe */ background-color: #f2f2f2; }.org-function-name { /* font-lock-function-name-face */ color: teal; }.org-header-line { /* header-line */ color: #333333; background-color: #e5e5e5; }.org-help-argument-name { /* help-argument-name */ font-style: italic; }.org-highlight { /* highlight */ background-color: #b4eeb4; }.org-holiday { /* holiday */ background-color: #ffc0cb; }.org-isearch { /* isearch */ color: #b0e2ff; background-color: #cd00cd; }.org-isearch-fail { /* isearch-fail */ background-color: #ffc1c1; }.org-italic { /* italic */ font-style: italic; }.org-keyword { /* font-lock-keyword-face */ color: #0086b3; }.org-lazy-highlight { /* lazy-highlight */ background-color: #afeeee; }.org-link { /* link */ color: #0000ff; text-decoration: underline; }.org-link-visited { /* link-visited */ color: #8b008b; text-decoratio
|
||
|
</style>
|
||
|
<style type="text/css">
|
||
|
h1,h2,h3,h4,h5,h6,legend{ font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif; font-weight:700; margin-top:0;}h1{ font-size:175%;}.subtitle{ font-size:95%; /* of h1 */}h2{ font-size:150%;}h3{ font-size:125%;}h4{ font-size:115%;}h5{ font-size:110%;}h6{ font-size:100%;}h4,h5,h6{ color:#2980B9; font-weight:300;}html{ -ms-text-size-adjust:100%; -webkit-text-size-adjust:100%; font-size:100%; height:100%; overflow-x:hidden;}body{ background:#edf0f2; color:#404040; font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif; font-weight:normal; margin:0; min-height:100%; overflow-x:hidden;}#content{ background:#fcfcfc; height:100%; margin-left:300px; /* margin:auto; */ max-width:800px; min-height:100%; padding:1.618em 3.236em;}p{ font-size:16px; line-height:24px; margin:0px 0px 24px 0px;}b,strong{ font-weight:bold}blockquote{ background-color: #F0F0F0; border-left:5px solid #CCCCCC; font-style:italic; line-height:24px; margin:0px 0px 24px 0px; /* margin-left:24px; */ padding: 6px 20px;}ul,ol,dl{ line-height:24px; list-style-image:none; /* list-style:none; */ margin:0px 0px 24px 0px; padding:0;}li{ margin-left: 24px;}dd{ margin:0;}#content .section ul,#content .toctree-wrapper ul,article ul{ list-style:disc; line-height:24px; margin-bottom:24px}#content .section ul li,#content .toctree-wrapper ul li,article ul li{ list-style:disc; margin-left:24px}#content .section ul li p:last-child,#content .toctree-wrapper ul li p:last-child,article ul li p:last-child{ margin-bottom:0}#content .section ul li ul,#content .toctree-wrapper ul li ul,article ul li ul{ margin-bottom:0}#content .section ul li li,#content .toctree-wrapper ul li li,article ul li li{ list-style:circle}#content .section ul li li li,#content .toctree-wrapper ul li li li,article ul li li li{ list-style:square}#content .section ul li ol li,#content .toctree-wrapper ul li ol li,article ul li ol li{ list-style:decimal}#content .section ol,#content ol,article ol{ list-style:decimal; line-height:24px; margin-bottom:24px}#content .section ol li,#content ol li,article ol li{ list-style:decimal; margin-left:24px}#content .section ol li p:last-child,#content ol li p:last-child,article ol li p:last-child{ margin-bottom:0}#content .section ol li ul,#content ol li ul,article ol li ul{ margin-bottom:0}#content .section ol li ul li,#content ol li ul li,article ol li ul li{ list-style:disc}dl dt{ font-weight:bold;}dl p,dl table,dl ul,dl ol{ margin-bottom:12px !important;}dl dd{ margin:0 0 12px 24px;}@media print{ .codeblock,pre.src{ white-space:pre.src-wrap}}@media print{ html,body,section{ background:none !important} *{ box-shadow:none !important; text-shadow:none !important; filter:none !important; -ms-filter:none !important} a,a:visited{ text-decoration:underline} pre.src,blockquote{ page-break-inside:avoid} thead{ display:table-header-group} tr,img{ page-break-inside:avoid} img{ max-width:100% !important} @page{ margin:0.5cm} p,h2,h3{ orphans:3; widows:3} h2,h3{ page-break-after:avoid}}@media print{ #postamble{ display:none} #content{ margin-left:0}}@media print{ #table-of-contents{ display:none} @page{ size: auto; margin: 25mm 25mm 25mm 25mm;} body { margin: 0px;}}@media screen and (max-width: 768px){}@media only screen and (max-width: 480px){}@media screen and (max-width: 768px){ .tablet-hide{ display:none}}@media screen and (max-width: 480px){ .mobile-hide{ display:none}}@media screen and (max
|
||
|
</style>
|
||
|
<script type="text/javascript">
|
||
|
/*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.3",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=a.length,c=n.type(a);
|
||
|
</script>
|
||
|
<script type="text/javascript">
|
||
|
!function(a,b){"use strict";function c(c,g){var h=this;h.$el=a(c),h.el=c,h.id=e++,h.$window=a(b),h.$document=a(document),h.$el.bind("destroyed",a.proxy(h.teardown,h)),h.$clonedHeader=null,h.$originalHeader=null,h.isSticky=!1,h.hasBeenSticky=!1,h.leftOffset=null,h.topOffset=null,h.init=function(){h.$el.each(function(){var b=a(this);b.css("padding",0),h.$originalHeader=a("thead:first",this),h.$clonedHeader=h.$originalHeader.clone(),b.trigger("clonedHeader."+d,[h.$clonedHeader]),h.$clonedHeader.addClass("tableFloatingHeader"),h.$clonedHeader.css("display","none"),h.$originalHeader.addClass("tableFloatingHeaderOriginal"),h.$originalHeader.after(h.$clonedHeader),h.$printStyle=a('<style type="text/css" media="print">.tableFloatingHeader{display:none !important;}.tableFloatingHeaderOriginal{position:static !important;}</style>'),a("head").append(h.$printStyle)}),h.setOptions(g),h.updateWidth(),h.toggleHeaders(),h.bind()},h.destroy=function(){h.$el.unbind("destroyed",h.teardown),h.teardown()},h.teardown=function(){h.isSticky&&h.$originalHeader.css("position","static"),a.removeData(h.el,"plugin_"+d),h.unbind(),h.$clonedHeader.remove(),h.$originalHeader.removeClass("tableFloatingHeaderOriginal"),h.$originalHeader.css("visibility","visible"),h.$printStyle.remove(),h.el=null,h.$el=null},h.bind=function(){h.$scrollableArea.on("scroll."+d,h.toggleHeaders),h.isWindowScrolling||(h.$window.on("scroll."+d+h.id,h.setPositionValues),h.$window.on("resize."+d+h.id,h.toggleHeaders)),h.$scrollableArea.on("resize."+d,h.toggleHeaders),h.$scrollableArea.on("resize."+d,h.updateWidth)},h.unbind=function(){h.$scrollableArea.off("."+d,h.toggleHeaders),h.isWindowScrolling||(h.$window.off("."+d+h.id,h.setPositionValues),h.$window.off("."+d+h.id,h.toggleHeaders)),h.$scrollableArea.off("."+d,h.updateWidth)},h.toggleHeaders=function(){h.$el&&h.$el.each(function(){var b,c=a(this),d=h.isWindowScrolling?isNaN(h.options.fixedOffset)?h.options.fixedOffset.outerHeight():h.options.fixedOffset:h.$scrollableArea.offset().top+(isNaN(h.options.fixedOffset)?0:h.options.fixedOffset),e=c.offset(),f=h.$scrollableArea.scrollTop()+d,g=h.$scrollableArea.scrollLeft(),i=h.isWindowScrolling?f>e.top:d>e.top,j=(h.isWindowScrolling?f:0)<e.top+c.height()-h.$clonedHeader.height()-(h.isWindowScrolling?0:d);i&&j?(b=e.left-g+h.options.leftOffset,h.$originalHeader.css({position:"fixed","margin-top":h.options.marginTop,left:b,"z-index":3}),h.leftOffset=b,h.topOffset=d,h.$clonedHeader.css("display",""),h.isSticky||(h.isSticky=!0,h.updateWidth()),h.setPositionValues()):h.isSticky&&(h.$originalHeader.css("position","static"),h.$clonedHeader.css("display","none"),h.isSticky=!1,h.resetWidth(a("td,th",h.$clonedHeader),a("td,th",h.$originalHeader)))})},h.setPositionValues=function(){var a=h.$window.scrollTop(),b=h.$window.scrollLeft();!h.isSticky||0>a||a+h.$window.height()>h.$document.height()||0>b||b+h.$window.width()>h.$document.width()||h.$originalHeader.css({top:h.topOffset-(h.isWindowScrolling?0:a),left:h.leftOffset-(h.isWindowScrolling?0:b)})},h.updateWidth=function(){if(h.isSticky){h.$originalHeaderCells||(h.$originalHeaderCells=a("th,td",h.$originalHeader)),h.$clonedHeaderCells||(h.$clonedHeaderCells=a("th,td",h.$clonedHeader));var b=h.getWidth(h.$clonedHeaderCells);h.setWidth(b,h.$clonedHeaderCells,h.$originalHeaderCells),h.$originalHeader.css("width",h.$clonedHeader.width())}},h.getWidth=function(c){var d=[];return c.each(function(c){var e,f=a(this);if("border-box"===f.css("box-sizing"))e=f[0].getBoundingClientRect().width;else{var g=a("th",h.$originalHeader);if("collapse"===g.css("border-collapse"))if(b.getComputedStyle)e=parseFloat(b.getComputedStyle(this,null).width);else{var i=parseFloat(f.css("padding-left")),j=parseFloat(f.css("padding-right")),k=parseFloat(f.css("border-width"));e=f.outerWidth()-i-j-k}else e=f.width()}d[c]=e}),d},h.setWidth=function(a,b,c){b.each(function(b){var d=a[b];c.eq(b).css({"min-width":d,"max-width":d})})},h.resetWidth=function(b,c){b.each(function(b){var d=a(this);c.eq(b).css({"min-width":d.css("min-width"),"max-width":d.css("max-width")})})},h.setOp
|
||
|
</script>
|
||
|
<script type="text/javascript">
|
||
|
if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.4",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.4",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this
|
||
|
</script>
|
||
|
<script type="text/javascript">
|
||
|
$(function() { $('.note').before("<p class='admonition-title note'>Note</p>"); $('.seealso').before("<p class='admonition-title seealso'>See also</p>"); $('.warning').before("<p class='admonition-title warning'>Warning</p>"); $('.caution').before("<p class='admonition-title caution'>Caution</p>"); $('.attention').before("<p class='admonition-title attention'>Attention</p>"); $('.tip').before("<p class='admonition-title tip'>Tip</p>"); $('.important').before("<p class='admonition-title important'>Important</p>"); $('.hint').before("<p class='admonition-title hint'>Hint</p>"); $('.error').before("<p class='admonition-title error'>Error</p>"); $('.danger').before("<p class='admonition-title danger'>Danger</p>");});$( document ).ready(function() { $(document).on('click', "[data-toggle='wy-nav-top']", function() { $("[data-toggle='wy-nav-shift']").toggleClass("shift"); $("[data-toggle='rst-versions']").toggleClass("shift"); }); $(document).on('click', ".wy-menu-vertical .current ul li a", function() { $("[data-toggle='wy-nav-shift']").removeClass("shift"); $("[data-toggle='rst-versions']").toggleClass("shift"); }); $(document).on('click', "[data-toggle='rst-current-version']", function() { $("[data-toggle='rst-versions']").toggleClass("shift-up"); }); $("table.docutils:not(.field-list)").wrap("<div class='wy-table-responsive'></div>");});$( document ).ready(function() { $('#text-table-of-contents ul').first().addClass('nav'); $('body').scrollspy({target: '#text-table-of-contents'}); var $postamble = $('#postamble'); var $tableOfContents = $('#table-of-contents'); $tableOfContents.css({paddingBottom: $postamble.outerHeight()}); var toggleSidebar = $('<div id="toggle-sidebar"><a href="#table-of-contents"><h2>Table of Contents</h2></a></div>'); $('#content').prepend(toggleSidebar); var closeBtn = $('<a class="close-sidebar" href="#">Close</a>'); var tocTitle = $('#table-of-contents').find('h2'); tocTitle.append(closeBtn);});window.SphinxRtdTheme = (function (jquery) { var stickyNav = (function () { var navBar, win, stickyNavCssClass = 'stickynav', applyStickNav = function () { if (navBar.height() <= win.height()) { navBar.addClass(stickyNavCssClass); } else { navBar.removeClass(stickyNavCssClass); } }, enable = function () { applyStickNav(); win.on('resize', applyStickNav); }, init = function () { navBar = jquery('nav.wy-nav-side:first'); win = jquery(window); }; jquery(init); return { enable : enable }; }()); return { StickyNav : stickyNav };}($));
|
||
|
</script>
|
||
|
</head>
|
||
|
<body>
|
||
|
<div id="content" class="content">
|
||
|
<h1 class="title">Duckbrain - Manual de inicio</h1>
|
||
|
<div id="table-of-contents" role="doc-toc">
|
||
|
<h2>Table of Contents</h2>
|
||
|
<div id="text-table-of-contents" role="doc-toc">
|
||
|
<ul>
|
||
|
<li><a href="#orgc479b92">1. Introducción</a>
|
||
|
<ul>
|
||
|
<li><a href="#org3d308e5">1.1. Estructura de archivos de duckbrain</a>
|
||
|
<ul>
|
||
|
<li><a href="#org148040b">1.1.1. Carpeta src</a></li>
|
||
|
<li><a href="#org36f1e61">1.1.2. Carpetas: Controllers, Libs, Models, Middlewares, Views y Routers</a></li>
|
||
|
<li><a href="#org7b8bba0">1.1.3. Los archivos en la raíz</a></li>
|
||
|
<li><a href="#org5188694">1.1.4. Los archivos en la carpeta Libs</a></li>
|
||
|
</ul>
|
||
|
</li>
|
||
|
<li><a href="#org5bd2ed4">1.2. Comprendiendo el arranque del sistema</a></li>
|
||
|
<li><a href="#org478b433">1.3. Extra: Namespaces</a></li>
|
||
|
</ul>
|
||
|
</li>
|
||
|
<li><a href="#org3366058">2. Primeros pasos</a>
|
||
|
<ul>
|
||
|
<li><a href="#org7a8b4f0">2.1. Usando un Webserver para el desarrollo</a></li>
|
||
|
<li><a href="#org1c422f7">2.2. Hola mundo</a>
|
||
|
<ul>
|
||
|
<li><a href="#org885e48a">2.2.1. Explicación del código del hola mundo</a></li>
|
||
|
</ul>
|
||
|
</li>
|
||
|
</ul>
|
||
|
</li>
|
||
|
<li><a href="#org0e2a82d">3. Mi primera aplicación</a>
|
||
|
<ul>
|
||
|
<li><a href="#org5d63ebf">3.1. Definición del proyecto</a></li>
|
||
|
<li><a href="#orgaf98bbe">3.2. Base de datos</a>
|
||
|
<ul>
|
||
|
<li><a href="#org1bbb6ad">3.2.1. Configuración de Duckbrain para la base de datos</a></li>
|
||
|
</ul>
|
||
|
</li>
|
||
|
<li><a href="#org4b29309">3.3. Modelos</a></li>
|
||
|
<li><a href="#orgfdeb754">3.4. Creando vistas</a></li>
|
||
|
<li><a href="#orga5ba85e">3.5. Mostrando el formulario</a>
|
||
|
<ul>
|
||
|
<li><a href="#orgd0bcca2">3.5.1. Extra: Explicación del tipo callable</a></li>
|
||
|
</ul>
|
||
|
</li>
|
||
|
<li><a href="#orgb48dcfe">3.6. Creando la nota encriptada</a></li>
|
||
|
<li><a href="#org162e7c5">3.7. Recta final: Desencriptando y mostrando las notas</a></li>
|
||
|
<li><a href="#orgf570fd9">3.8. Repositorio</a></li>
|
||
|
</ul>
|
||
|
</li>
|
||
|
<li><a href="#org94b4628">4. Contacto</a></li>
|
||
|
</ul>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-orgc479b92" class="outline-2">
|
||
|
<h2 id="orgc479b92"><span class="section-number-2">1.</span> Introducción</h2>
|
||
|
<div class="outline-text-2" id="text-1">
|
||
|
<p>
|
||
|
Duckbrain es un conjunto de librerías creadas especialmente pensando en hacer algo simple, que cuarquiera pueda deshacer y rearmar fácilmente. Gracias a esa versatilidad, puedes usarlo cualquier proyecto, peo por ello debes tener en cuenta que mientras más grande sea el proyecto, mayor será el nivel de ingeniería que se requerirá de tu parte para acondicionar esta herramienta a los requerimientos.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Otro punto fuerte que puedo dar de esta herramienta, es que te puede ser tremendamente útil si estás comenzando, ya que al ser tan pequeño y modificable, es más sencillo que lo comprendas y esto más adelante te ayudará a comprender cosas más grandes.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Ducho eso, vamos a comenzar…
|
||
|
</p>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org3d308e5" class="outline-3">
|
||
|
<h3 id="org3d308e5"><span class="section-number-3">1.1.</span> Estructura de archivos de duckbrain</h3>
|
||
|
<div class="outline-text-3" id="text-1-1">
|
||
|
<p>
|
||
|
Al entrar a la carpeta de duckbrain encontraras esta estructura de archivos (ignoramos el readme.org porque no es necesario).
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-ditaa">.
|
||
|
├── config.php
|
||
|
├── index.php
|
||
|
├── .htaccess
|
||
|
└── src
|
||
|
├── Controllers
|
||
|
├── Libs
|
||
|
│ ├── Database.php
|
||
|
│ ├── Middleware.php
|
||
|
│ ├── Model.php
|
||
|
│ ├── Neuron.php
|
||
|
│ ├── Request.php
|
||
|
│ ├── Router.php
|
||
|
│ └── View.php
|
||
|
├── Middlewares
|
||
|
├── Models
|
||
|
├── Routers
|
||
|
└── Views
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Primero veamos las carpetas y luego los archivos:
|
||
|
</p>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org148040b" class="outline-4">
|
||
|
<h4 id="org148040b"><span class="section-number-4">1.1.1.</span> Carpeta src</h4>
|
||
|
<div class="outline-text-4" id="text-1-1-1">
|
||
|
<p>
|
||
|
En esta carpeta se encuentra todo el código PHP.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Como todo en Duckbrain, esta ruta se puede cambiar editando una variable en el archivo <code>index.php</code>.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org36f1e61" class="outline-4">
|
||
|
<h4 id="org36f1e61"><span class="section-number-4">1.1.2.</span> Carpetas: Controllers, Libs, Models, Middlewares, Views y Routers</h4>
|
||
|
<div class="outline-text-4" id="text-1-1-2">
|
||
|
<p>
|
||
|
Si sabes de estructuras de diseño, el nombre de estas carpetas ya te debe quedar claro qué va en cada una de ellas, puesto que en el nombre de la carpeta dice lo que va a ir dentro.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Si no sabes que es un Model, un Middleware, Controller, etc. entonces requieres buscar ahora mismo lo que es una arquitectura de diseño de software y leerte al menos en qué consiste la arquitectura de MVC, ya que es una arquitectura simple y la que usaremos más adelante en este manual.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Al igual que con la carpeta <code>src</code>, todo lo puedes cambiar y colocar otra estructura de carpetas y las únicas que requerirían algún tipo de edición de archivos serían las carpetas <code>Views</code> y <code>Router</code> que se editarían en el archivo <code>src/Libs/View.php</code> y el archivo <code>index.php</code> respectivamente. Las demás carpetas las puedes borrar cuando quieras y usar otra estructura, pero por lo pronto no lo hagas al menos hasta que hayas pasado la sección <a href="#org0e2a82d">3</a> que es cuando ya habrías leído y, con algo de suerte y pericia, asimilado los conocimientos necesarios para hacer lo que te de la gana con este ser de librería al punto de que ya le podrás colocar las tuyas propias, le borrarás alguna de las que viene por defecto porque no la necesites o incluso crees una versión extendida de las que viene por defecto para poder añadirle alguna mejora que necesites.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Desde luego, si eres un ansias y sabes lo que son las arquitecturas de diseño, ya has programado en alguna arquitectura de diseño como DDD, MVC, TDD, EDD, etc. antes y sabes PHP, con que te leas el código del archivo <code>index.php</code> (son solo 20 líneas de código) y tengas el nivel de PHP para entenderlo y programar en la arquitectura de diseño que desees, entonces con ver esas 20 líneas te deberá bastar y puedes saltarte el resto del este manual y guiarte de ahora en adelante con la documentación API de las librerías o directamente con el LSP de tu editor de código para poco que queda.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Para el resto de mortales y gente con menos arrogancia, pueden seguir un ratito más por aquí :P.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org7b8bba0" class="outline-4">
|
||
|
<h4 id="org7b8bba0"><span class="section-number-4">1.1.3.</span> Los archivos en la raíz</h4>
|
||
|
<div class="outline-text-4" id="text-1-1-3">
|
||
|
<p>
|
||
|
El <code>.htaccess</code> es un archivo que te será útil si usas apache o algún otro servidor web que use estos archivos y lo que lleva es una configuración especial para que funcionen las <b>rutas virtuales</b>, o sea, para que podamos crear una ruta tipo <code>mitsitio.com/mi-ruta-virtual/</code> y podamos decir que allí muestre un mensaje de <code>Hola mundo</code>, a pesar de que la carpeta <code>mi-ruta-virtual</code> no exista, ni haya dentro ningún archivo index que tenga el texto <code>Hola mundo</code>. Si esta aún no lo entendiste, no exasperes que ya lo vas a entender en un momento y ahí podrás volver a repasar esta parte.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
En caso de que uses nginx, debes configurar el webserver con la regla equivalente al contenido del archivo <code>.htacess</code> que sería más o menos esto:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-nginx">location / {
|
||
|
try_files $uri $uri/ ./index.php$args;
|
||
|
}
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
El archivo <code>index.php</code> es el punto de entrada de nuestra aplicación, aquí sucederá toda la magia inicial y sólo requiere de 20 míseras lineas para hacer ese gran trabajo y soportar sistemas tan grandes sin despeinarse ante frameworks más complejos como laravel o que son usados por cientos de miles de usuarios en cientos de instalaciones. Esto lo menciono no por creer que Duckbrain podría soportarlo, sino porque es algo que ya sucede, o sea, dichos sistemas ya existen: Los he hecho yo o he estado involucrado en parte del proyecto al punto de poder elegir Duckbrain como base para el mismo.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
El archivo <code>config.php</code>, como su nombre indica, es el archivo para colocar las configuraciones.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org5188694" class="outline-4">
|
||
|
<h4 id="org5188694"><span class="section-number-4">1.1.4.</span> Los archivos en la carpeta Libs</h4>
|
||
|
<div class="outline-text-4" id="text-1-1-4">
|
||
|
<p>
|
||
|
Son básicamente librerías, cada una tiene como nombre lo que hace, no tiene mucho misterio y al igual que antes con las carpetas, si sabes algo de diseño, con leer el nombre de las mismas te debería bastar para saber lo que hacen. Si no lo entiendes, sigue leyendo el manual que hablaremos de casi todas poco a poco.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org5bd2ed4" class="outline-3">
|
||
|
<h3 id="org5bd2ed4"><span class="section-number-3">1.2.</span> Comprendiendo el arranque del sistema</h3>
|
||
|
<div class="outline-text-3" id="text-1-2">
|
||
|
<p>
|
||
|
Como la intención de este manual es que comprendas Duckbrain y no solo seas un robot que copia y pega de la documentación, vamos a calentar comprendiendo el código que arranca Duckbrain.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Como ya mencionamos antes, el archivo <code>.htaccess</code> se encarga de hacer funcionar las rutas virtuales, pero eso como programador que eres te debería hacer saltar una pregunta "¿Cómo es que hace eso?" y la respuesta es simple y puede que ya lo entiendas gracias al código de configuración de nginx que coloqué antes:
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Supongamos que entramos en la ruta <code>/hola</code> (o sea, <code>misitio.com/hola</code>), lo que hará el <code>.htacess</code> o la configuración nginx será lo siguiente:
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Primero intenta comprobar si el fichero "hola" existe en la ruta a la que se desea acceder, si no es el caso prueba si existe una carpeta "hola" y si tampoco sucede eso, reenvía todo a <code>index.php</code> y le pide que él se encargue de ahí en adelante. Si llega hasta la tercera opción decimos que es una <b>ruta virtual</b> (lo que mencionamos en <a href="#org7b8bba0">Los archivos en la raiz</a>), porque devolveremos algo desde el PHP simulando un archivo o una carpeta, a pesar de que esa ruta realmente no existe ni como fichero ni como carpeta.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Ahora veamos que hace <code>index.php</code>, en la primera línea nos dice esto:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">require_once('config.php');
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Aquí carga el archivo <code>config.php</code>, o sea, carga la configuración. Para esta parte del manual, solo nos interesa la última línea ya que todo lo demás en realidad no está configurando nada aún. Lo que dice la línea es esto:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">define('ROOT_DIR', __DIR__);
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Solo está definiendo una constante <code>ROOT_DIR</code> que contiene como valor el directorio en donde está el archivo <code>config.php</code>, que también sería la raíz de Duckbrain.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
La siguiente línea también es la definición de una constante:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">define('ROOT_CORE', ROOT_DIR.'/src');
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Esta constante <code>ROOT_CORE</code> lo que hace es tener la ruta de src. Hasta aquí acabamos con la definición de constantes, ahora el bloque que viene es donde se poner lo bueno:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">spl_autoload_register(function ($className) {
|
||
|
$fp = str_replace('\\','/',$className);
|
||
|
$name = basename($fp);
|
||
|
$dir = dirname($fp);
|
||
|
$file = ROOT_CORE.'/'.$dir.'/'.$name.'.php';
|
||
|
if (file_exists($file)) {
|
||
|
require_once $file;
|
||
|
return;
|
||
|
}
|
||
|
});
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Este bloque define un autoloader para PHP. Como su nombre lo indica, lo que hace es cargar los archivos en función del su clase, considerando siempre que dichos archivos estarán dentro de la carpeta configurada en <code>ROOT_CORE</code>, que generalmente será <code>src</code>.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Si aún no te ha hecho clic lo bonito que es tener un autoloader como este, básicamente lo que sucede es que en lugar de que a cada archivo que uses lo tengas que cargar usando la función <code>include()</code>, <code>require()</code>, <code>include_once()</code> o <code>require_once()</code>, bastará solo con usar la clase que contiene, siguiendo la convención de que el <code>namespace</code> indica las carpetas y el nombre del archivo tiene el mismo nombre de la clase.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Si aún no lo entendiste, no pierdas tiempo releyendo el párrafo anterior, en seguida lo vamos a retomar y a explicar mejor. Por lo pronto vamos a por el bloque final:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">$routers = glob(ROOT_CORE.'/Routers/*.php');
|
||
|
|
||
|
foreach($routers as $file){
|
||
|
require_once($file);
|
||
|
}
|
||
|
|
||
|
\Libs\Router::apply();
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
La primera línea de código lista todos los archivos <code>.php</code> dentro de nuestra carpeta <code>src/Routers/</code>, la segunda parte los incluye todos esos archivos PHP, o sea, ejecuta el código PHP que tengan dentro y la última llama a la clase <code>\Lib\Router::apply()</code> y aquí hacemos uso del bloque anterior de la siguiente manera:
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Hasta ahora, el único archivo que hemos cargado es el de configuración en la primera línea. La clase <code>\Libs\Router</code> realmente no está definida y en un PHP normal nos daría error por ello, pero aquí viene al rescate nuestro autoloader, ya que el toma la clase y como se llama <code>\Lib\Router</code> comprende que tiene que intentar cargar el archivo en <code>src/Libs/Router.php</code> que es donde vendría a estar la clase que estamos necesitando. Como el archivo existe y la clase está definida allí, es capaz de cargarla y de correr la función estática <code>apply()</code> y así es como todos terminan felices sin dar ningún error :).
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Nótese que lo que ha hecho el autoloader es convertir los backslashes o barras invertidas en slashes o barrar normales y luego concatenarlas con <code>ROOT_CORE</code> para obtener la ruta de nuestro archivo. Esto es muy importante, así que si no lo entiendes léelo nuevamente (explicación y código) o pide a algún amigo, conocido o random de internet en algún foro o chat de telegram, matrix, steam, discord, reddit, etc. que te lo explique hasta que comprendas perfectamente ese fracmento de código.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org478b433" class="outline-3">
|
||
|
<h3 id="org478b433"><span class="section-number-3">1.3.</span> Extra: Namespaces</h3>
|
||
|
<div class="outline-text-3" id="text-1-3">
|
||
|
<p>
|
||
|
Si eres nuevo en esto puede que no sepas lo que es un namespace, pero si también te dio curiosidad el que la una clase tenga <code>\</code> (backslash) en su nombre, entonces tienes futuro en este campo y como no quiero que te vayas de este manual, voy a explicarte rápidamente de donde viene ese backslash.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Si abres el archivo <code>src/Libs/Router.php</code> verás que en su primera línea dice esto:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">namespace Libs;
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Esa línea generalmente será la primera siempre de un archivo PHP que use namespaces, no puede haber ninguna otra línea de código. Lo que hace dicha línea es darle una "familia" a la clase y con dicha familia, también viene el apellido de la misma.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
De modo que cualquier clase que se cree dentro de un archivo con un namespace, de ahora en adelante tendrá como "apellido" ese namespace en su nombre. De ese modo, como en el archivo Router declaramos la clase <code>Router</code>, en realidad estamos creando la clase <code>Libs\Router</code> (el backslash del inicio se puede omitir).
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Puede que ahora mismo no notes la utilidad de esto, pero es algo muy útil realmente y ya verás más adelante la magia que supone.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org3366058" class="outline-2">
|
||
|
<h2 id="org3366058"><span class="section-number-2">2.</span> Primeros pasos</h2>
|
||
|
<div class="outline-text-2" id="text-2">
|
||
|
<p>
|
||
|
Antes de ponernos a programar vamos a asegurarnos de estar en la misma pagina preparando nuestro entorno de trabajo y haciendo el usual Hola mundo.
|
||
|
</p>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org7a8b4f0" class="outline-3">
|
||
|
<h3 id="org7a8b4f0"><span class="section-number-3">2.1.</span> Usando un Webserver para el desarrollo</h3>
|
||
|
<div class="outline-text-3" id="text-2-1">
|
||
|
<p>
|
||
|
Desde luego esta la carpeta con Duckbrain supongo que ya la tienes con un webserver nginx o apache en localhost. Si no es el caso, al menos deberás instalarte PHP primero y una ves instalado, abres una terminal en la carpeta donde tienes Duckbrain y ejecutas:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-bash">php -S localhost:80
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Eso te creará un webserver que podrás acceder escribiendo en el navegador <code>http://localhost</code> y verás lo que estemos corriendo en el proyecto.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
En la terminal, ahora verás los logs de acceso y en caso de que tu código tenga un error, igual aparecerá allí. En el caso de Linux, MAC y FreeBSD los accesos se verán en amarillo para errores no fatales como archivos inexistente y en rojo si es un error fatal que detiene la ejecución de PHP. En Windows no estoy seguro.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
He conocido gente que usa directamente un hosting y edita los archivos allí. Eso no está mal si estás aprendiendo o para hacer código rápido, pero en lo personal, siempre recomiendo desarrollar en local, ya que es más rápido y con herramientas como Docker o KVM puedes simular la misma configuración que tendría un servidor en producción, con los retoques que necesites para tu entorno de desarrollo.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org1c422f7" class="outline-3">
|
||
|
<h3 id="org1c422f7"><span class="section-number-3">2.2.</span> Hola mundo</h3>
|
||
|
<div class="outline-text-3" id="text-2-2">
|
||
|
<p>
|
||
|
Agarren su teclado y su editor de código que vamos a comenzar a hacer la magia. Como es usual, el primer paso para hacer nuetra aplicación, será imprimir un <code>hello world</code>.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Para ello vamos a la carpeta <code>src/Routers</code> y crearemos un archivo con el nombre de <code>main.php</code> y dentro colocaremos el siguiente código:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
use Libs\Request;
|
||
|
use Libs\Router;
|
||
|
|
||
|
Router::get('/', function(Request $requets) {
|
||
|
echo "Hola mundo";
|
||
|
});
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
|
||
|
<p>
|
||
|
Ahora, siguiendo con el código, abrimos la URL donde estamos corriendo el sitio (normalmente localhost) y veremos nuestro esperado "hola mundo".
|
||
|
</p>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org885e48a" class="outline-4">
|
||
|
<h4 id="org885e48a"><span class="section-number-4">2.2.1.</span> Explicación del código del hola mundo</h4>
|
||
|
<div class="outline-text-4" id="text-2-2-1">
|
||
|
<p>
|
||
|
Si entendiste el código con solo leerlo, perfecto, pero si no lo hiciste, vamos a explicarlo. Primero vamos a explicar esta parte:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">Router::get('/', function(Request $requets) {
|
||
|
echo "Hola mundo";
|
||
|
});
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Lo que estámos haciendo aquí, es llamar al método <code>get</code> de la clase <code>Router</code>. La clase, como indica su nombre, maneja las rutas, el método <code>get</code> indica el <a href="https://es.wikipedia.org/wiki/Protocolo_de_transferencia_de_hipertexto#M%C3%A9todos_de_petici%C3%B3n">método HTTP</a> que vamos a configurar (Duckbrain soporta los métodos HTTP: get, post, put, patch y delete).
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
<b>Los parámetros que recibe la función son 2:</b>
|
||
|
</p>
|
||
|
<ul class="org-ul">
|
||
|
<li><span class="underline">La ruta que vamos a configurar:</span> En este caso es <code>/</code> que es la raíz de nuestro dominio, o sea, <code>http://localhost/</code>. Prueba ahora mismo cambiar eso a <code>/hola</code> y verás que al refrescar ahora da <code>error 404</code>, pero al entrar en <code>http://localhost/hola</code> nuevamente podrás ver el mensaje <code>Hola mundo</code>.</li>
|
||
|
<li><span class="underline">Una función anónima:</span> Por lo pronto quédate que en ese segundo parámetro va un elemento <code>callable</code>, en este caso es una función anónima, lo cual entra dentro de las cosas que son <code>callable</code>. Este segúndo parámetro, como se puede deducir con facilidad, es lo que se ejecutará cuando ingresemos a la ruta configurada en el primer parámetro.</li>
|
||
|
</ul>
|
||
|
|
||
|
<p>
|
||
|
<b>La palabra reservada use</b>:
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Al igual que en <a href="#org478b433">Extra: Namespaces</a> la explicación de las primeras 2 líneas de este código, realmente no es algo de Duckbrain, sino del lenguaje mismo, pero de todos modos lo explicaré.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Primero que nada, si no quisieramos o no tuviéramos la posibilidad de usar <code>use</code>, nuestro código se vería así:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
Libs\Router::get('/', function(Libs\Request $requets) {
|
||
|
echo "Hola mundo";
|
||
|
});
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Puedes probarlo y verás que funciona perfectamente. El problema es que en códigos más largos, con namespaces más largos, sería demasiado tedioso escribir el nombre completo de las clases todo el tiempo, del mismo modo que en la vida real sería tedioso referirnos a las personas siempre por sus nombres completos y en su lugar los llamamos por su nombre de pila o un apodo.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Si un <code>namespace</code> es como dar un apellido, <code>use</code> es como dar un apodo o nombre temporal. Por defecto, ese apodo o nombre local será el nombre de pila de la clase, pero igual podemos definirlo de la siguente manera:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">use Libs\Router as Rutercito;
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
En este caso, estamos usando la plabra reservada <code>as</code> para definir arbitrariamente que el apodo de <code>Libs\Router</code> será, <code>Rutercito</code>. El código ahora entenderá a qué clase nos referimos ya sea que usemos el nombre completo o el apodo <code>Routecito</code>.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Prueba ponerle apodos ambas clases y usar sus distintos nombres en el código. Es importante que siempre que aprendas, pruebes por cuenta propia las cosas, así te aburres menos y es mas fácil que lo comprendas.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org0e2a82d" class="outline-2">
|
||
|
<h2 id="org0e2a82d"><span class="section-number-2">3.</span> Mi primera aplicación</h2>
|
||
|
<div class="outline-text-2" id="text-3">
|
||
|
<p>
|
||
|
Ya hicimos un hola mundo, ahora vamos a hacer algo completo de verdad. Una buena práctica a la hora de crear proyectos es comenzar a definir lo que vamos a hacer, por lo tanto:
|
||
|
</p>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org5d63ebf" class="outline-3">
|
||
|
<h3 id="org5d63ebf"><span class="section-number-3">3.1.</span> Definición del proyecto</h3>
|
||
|
<div class="outline-text-3" id="text-3-1">
|
||
|
<p>
|
||
|
Vamos a hacer un sistema que nos permita compartir textos de manera secreta. Una persona cualquiera podrá entrar a nuestra web, escribir un texto o nota secreta y se le entregará un enlace con el que puede compartir esa nota secreta.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Como medida de seguridad, existirá una llave secreta que se usará para encriptar la nota y dicha llave secreta no se guardará en nuestra base de datos, sino que irá en el enlace que le daremos a la persona a la hora de crear su texto secreto. De modo que las únicas personas capaces de desencriptar ese texto secreto, serán aquellas que tengan ese enlace.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Como extra, la notas tendrán siempre un tiempo de expiración definido por el creador de la nota y/o una cantidad de lecturas máximas, luego de lo cual ya se podrán desencriptar y serán eliminadas de nuestra base de datos si alguien intenta leerlas.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
El nombre de la aplicación será <b>Ignota</b>. No soy bueno con los nombres y pensaba que podría llamarla Ignita nota (nota de fuego en latín) e intentando crear un apócope, llegué a una palabra que ya existe en español y además tiene un significado acorde, además de tener la palabra "nota" en el nombre :).
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-orgaf98bbe" class="outline-3">
|
||
|
<h3 id="orgaf98bbe"><span class="section-number-3">3.2.</span> Base de datos</h3>
|
||
|
<div class="outline-text-3" id="text-3-2">
|
||
|
<p>
|
||
|
Este sistema no es algo complicado en lo que al diseño de la base de datos se refiere, por lo que sólo vendría a tener una tabla:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-plantuml">|-----------|
|
||
|
| notes |
|
||
|
|-----------|
|
||
|
| id |
|
||
|
| content |
|
||
|
| expire_at |
|
||
|
| max_views |
|
||
|
|-----------|
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
El código para crear las tablas en SQLite sería este:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-sqlite">CREATE TABLE notes (
|
||
|
id INTEGER PRIMARY KEY,
|
||
|
content TEXT,
|
||
|
expire_at INTEGER,
|
||
|
max_views INTEGER
|
||
|
);
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Duckbrain soporta también MySQL/MariaDB, por lo que si quisieramos usar dicho gestor, el código sería así:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-sql">CREATE TABLE notes (
|
||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||
|
content TEXT,
|
||
|
expire_at INT default null,
|
||
|
max_views INT default 0
|
||
|
);
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Voy a obviar la parte de cómo crear las bases de datos ya que ambas cosas se pueden encontrar fácil, ya sea preguntando o buscando por cuenta propia. Desde luego, si son muy nuevos, recomiendo que usen SQLite que es la más sencilla de las 2 opciones y por ello también usaremos esa opción de ahora en adelante.
|
||
|
</p>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org1bbb6ad" class="outline-4">
|
||
|
<h4 id="org1bbb6ad"><span class="section-number-4">3.2.1.</span> Configuración de Duckbrain para la base de datos</h4>
|
||
|
<div class="outline-text-4" id="text-3-2-1">
|
||
|
<p>
|
||
|
Desde luego, Duckbrain va necesitar tener la información de la base de datos, por lo que abriremos el archivo y veremos algo como esto:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
define('DB_TYPE', 'mysql');
|
||
|
define('DB_HOST', 'localhost');
|
||
|
define('DB_NAME', '');
|
||
|
define('DB_USER', '');
|
||
|
define('DB_PASS', '');
|
||
|
|
||
|
//define('SITE_URL', '');
|
||
|
|
||
|
define('ROOT_DIR', __DIR__);
|
||
|
?>
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
La primera línea define el tipo de base de datos que vamos a usar, mientras que el resto su información. Ya que estamos, voy a configurar también SITE<sub>URL</sub> que en realida no es necesario, pero quiero hacerlo para dar a notar que en caso de configurarlo, es obligatorio que la url en termine en <code>/</code>.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
El código terminaría así:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
define('DB_TYPE', 'sqlite');
|
||
|
define('DB_HOST', 'localhost');
|
||
|
define('DB_NAME', 'mi-bd.sqlite');
|
||
|
define('DB_USER', '');
|
||
|
define('DB_PASS', '');
|
||
|
|
||
|
define('SITE_URL', 'http://localhost/');
|
||
|
|
||
|
define('ROOT_DIR', __DIR__);
|
||
|
?>
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Como nuestra base de dato es solo un archivo sqlite, no requerimos de <code>DB_HOST</code>, <code>DB_USER</code> ni <code>DB_PASS</code>. Todas esas líneas podemos eliminarlas o dejarlas tal cual, pues no afectarán en nada.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org4b29309" class="outline-3">
|
||
|
<h3 id="org4b29309"><span class="section-number-3">3.3.</span> Modelos</h3>
|
||
|
<div class="outline-text-3" id="text-3-3">
|
||
|
<p>
|
||
|
Debo hacer incapié en que nada está escrito en piedra y podemos hacer las cosas de muchas manera con Duckbrain, pero en este caso, siguiendo la estructura MVC, vamos a crear modelos, vistas y controladores.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Comenzando con los modelos, que en realidad para este proyecto sería en singular, vamos a crear un archivo llamado <code>Note.php</code> en <code>src/Models</code> con el siguiente contenido:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
namespace Models;
|
||
|
|
||
|
use Libs\Model;
|
||
|
|
||
|
class Note extends Model {
|
||
|
public string $content;
|
||
|
public ?int $expire_at;
|
||
|
public int $max_views = 0;
|
||
|
}
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
En el caso de los modelos que extienden de <code>Libs\Model</code> es necesario poner como públicos los nombres de las variables que queremos que se guarden o traigan de la base de datos. Del mismo modo es importante el nombre de la clase, ya que esta se pasará de <code>PascalCase</code> a <code>snake_case</code> en plural (añadiendo una <code>s</code> al final), para usarlo como tabla, o dicho más simple, como la clase se llama <code>Note</code>, la tabla con la que estará asociada será <code>notes</code>. Si por otro lado la clase se llamara <code>EncriptedNote</code>, la tabla en la base de datos tendría que llamarse <code>encripted_notes</code>.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Repitiendo otra vez lo antes dicho ya muchas veces, nada está escrito en piedra y si quieres usar un nombre de la tabla y no hacer uso de la conversión de nomenclaturas antes mencionada, puedes hacerlo definiendo la propiedad protegida <code>table</code>. Del mismo modo, si quieres tener alguna propiedad privada o protegida en lugar de pública, pero igualmente quieres que se guarde, solo debes definirlo en la propiedad <code>forceSave</code> y si lo que quieres es todo lo contrario (que una propiedad pública no se guarde), entonces podrías hacer uso de la propiedad <code>ignoreSave</code>.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Nada de lo mencionado en el anterior párrafo lo necesitaremos, pero de todos modos aquí tienes un ejemplo de como se vería:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
namespace Models;
|
||
|
|
||
|
use Libs\Model;
|
||
|
|
||
|
class Note extends Model {
|
||
|
public string $content;
|
||
|
public ?int $expire_at;
|
||
|
public int $max_views = 0;
|
||
|
public string $tmp_cache;
|
||
|
private string $password;
|
||
|
protected string $salt;
|
||
|
|
||
|
// Cambiamos el nombre de la tabla a la que se relacionará nuestro modelo.
|
||
|
static protected string $table = 'se_llama_como_YO_quiero_AAAhh';
|
||
|
|
||
|
// Forzamos a que se guarden las propiedades salt y password a pesar de que no son públicas.
|
||
|
static protected array $forceSave = ['password', 'salt'];
|
||
|
|
||
|
// Frozamos a no guuardar la propiedad pública tmp_cache
|
||
|
static protected array $ignoreSave = ['tmp_cache'];
|
||
|
}
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
<b>Nota:</b> En esta ocasión no explicaré qué es lo que hace la palabra reservada <code>extends</code>. Si no comprendes lo que hace, por favor busca y pregunta ya que es algo extremadamente importante de comprender, además de ser algo que se usa en prácticamente todos los lenguajes de programación que soportan clases.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-orgfdeb754" class="outline-3">
|
||
|
<h3 id="orgfdeb754"><span class="section-number-3">3.4.</span> Creando vistas</h3>
|
||
|
<div class="outline-text-3" id="text-3-4">
|
||
|
<p>
|
||
|
En este caso solo necesitaremos una sola vista, por ello no crearemos archivos aparte para el código CSS, sino que irá todo en un solo archivo.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Vamos a crear un archivo llamado <code>CreateNote.php</code> en <code>src/Views/</code> con el siguiente código:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-html"><!DOCTYPE html>
|
||
|
<html>
|
||
|
<head>
|
||
|
<meta charset="UTF-8"/>
|
||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
<title>Ignota - Notas encriptadas punto a punto</title>
|
||
|
<style>
|
||
|
form {
|
||
|
display: grid;
|
||
|
place-content: center;
|
||
|
gap: 5px;
|
||
|
max-height: 100vh;
|
||
|
min-height: 50vh;
|
||
|
text-align: center;
|
||
|
}
|
||
|
|
||
|
textarea {
|
||
|
min-width: 80vw;
|
||
|
max-width: 95vw;
|
||
|
min-height: 50vh;
|
||
|
}
|
||
|
|
||
|
button {
|
||
|
padding: 5px;
|
||
|
font-size: 18px;
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
|
||
|
.config {
|
||
|
display: grid;
|
||
|
grid-template-columns: auto 100px;
|
||
|
text-align: left;
|
||
|
gap: 5px;
|
||
|
justify-content: center;
|
||
|
}
|
||
|
|
||
|
legend {
|
||
|
font-size: 15px;
|
||
|
font-weight: bold;
|
||
|
display: ;
|
||
|
}
|
||
|
</style>
|
||
|
</head>
|
||
|
<body>
|
||
|
<form method="POST">
|
||
|
<textarea name="note" placeholder="Escribe tu texto a encriptar"></textarea>
|
||
|
<fieldset class="config">
|
||
|
<legend>Configuración de expiración</legend>
|
||
|
<label for="max_views">Visualizaciones máximas (0 = infinitas):</label>
|
||
|
<input name="max_views" type="int" value="0">
|
||
|
<label for="expiration">Tiempo de expiración (en minutos):</label>
|
||
|
<input name="expiration" type="int" value="15">
|
||
|
</fieldset>
|
||
|
<button type="submit">Guardar</button>
|
||
|
</form>
|
||
|
</body>
|
||
|
</html>
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
No creo que sea necesario que expliquemos un código html, si no lo entiendes, no te quedes con la duda y busca comprenderlo/aprenderlo.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-orga5ba85e" class="outline-3">
|
||
|
<h3 id="orga5ba85e"><span class="section-number-3">3.5.</span> Mostrando el formulario</h3>
|
||
|
<div class="outline-text-3" id="text-3-5">
|
||
|
<p>
|
||
|
Ahora no vamos a hacer un archivos completos, los haremos a pedacitos de código a medida que avancemos.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Comenzando con el controlador, vamos a crear un archivo llamado <code>NoteController.php</code> en la carpeta <code>src/Controllers/</code> y colocaremos dentro este código:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
namespace Controllers;
|
||
|
|
||
|
use Libs\Request;
|
||
|
use Libs\View;
|
||
|
|
||
|
class NoteController {
|
||
|
public static function home(Request $request): void
|
||
|
{
|
||
|
View::render('CreateNote');
|
||
|
}
|
||
|
}
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Aquí lo que hemos hecho ha sido crear una función estática dentro de nuestro controlador, el ejecutará el metodo <code>render</code>, de la librería <code>View</code> (<code>Libs\View</code>) el cual en de manera interna en lo que hace es crear un objeto de tipo <code>View</code> y ejecutar el método <code>html</code> con los mismos argumentos que recibe el método render. Puedes comprobarlo tú mismo leyendo el código de esa librería, es cortito y no muerde.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Luego eso lo que teminará haciendo es buscar un archivo llamado <code>CreateNote.php</code> dentro de <code>src/Views/</code> y lo cargará, de modo que terminaríamos viendo el formulario que creamos antes.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Sin embargo, aún nos falta un paso para que ese formulario se vea y es configurar la ruta donde se verá. Para hacerlo, vamos a renombrar el archivo <code>main.php</code> que creamos antes y como estas rutas que configuraremos son las rutas de nuestras notas, un buen nombre a usar sería <code>note.php</code>, si luego tuvieramos, usuarios entonces podríamos crear otro archivos llamado <code>user.php</code> y poner allí todas las rutas que sean de usuarios.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Bueno, siguendo a lo que íbamos. Si es que has estado tomando antención, el contenido de este archivo tiene que ser la configuración de una ruta que ejecute la función <code>home</code> que creamos antes en el controlador.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Aquí me doy el lujo de hacer otro paréntesis para hablar del tipo <code>callable</code> de PHP. Esto es un tipo especial, ya que no es una clase realmente, de hecho puede ser un <code>array</code>, una función anónima o un <code>string</code> sin problemas. La única condición es que debe ser algo que puedas llamar como función, de modo que el contenido de nuestro archivo <code>note.php</code> lo podremos poner de una de estas 2 maneras:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
use Libs\Router;
|
||
|
|
||
|
Router::get('/', 'Controllers\NoteController::home');
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
O de esta otra manera:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
use Controllers\NoteController;
|
||
|
use Libs\Router;
|
||
|
|
||
|
Router::get('/', [NoteController::class, 'home']);
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Ambas funcionarán y al entrar en <code>http://localhost</code> ya
|
||
|
</p>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-orgd0bcca2" class="outline-4">
|
||
|
<h4 id="orgd0bcca2"><span class="section-number-4">3.5.1.</span> Extra: Explicación del tipo callable</h4>
|
||
|
<div class="outline-text-4" id="text-3-5-1">
|
||
|
<p>
|
||
|
Enlos anteriores 2 bloques de código lo que hemos visto han sido 2 maneras de escribir un callable:
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
En el primer código he escrito el nombre completo hasta la función en un string, mientras que en el segundo hemos hecho lo mismo, pero en forma de array. Ambos funcionan como <code>callables</code>, ya que se podrían ejecutar como función y puedes comprobarlo tú mismo intentando correr el siguiente código:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
use Controllers\NoteController;
|
||
|
use Libs\Request;
|
||
|
use Libs\Router;
|
||
|
|
||
|
Router::get('/', function(){});
|
||
|
[NoteController::class, 'home'](new Request);
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
O también:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php"><?php
|
||
|
use Libs\Request;
|
||
|
use Libs\Router;
|
||
|
|
||
|
Router::get('/', function(){});
|
||
|
'Controllers\NoteController::home'(new Request);
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Sé que esto es algo raro y que realmente tiene que ver con el conocimiendo de PHP, pero tenía que mencionarlo porque me he topado con gente que usa frameworks como laravel y creen que eso es algo que sucede por "la magia del framework" y es un error.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-orgb48dcfe" class="outline-3">
|
||
|
<h3 id="orgb48dcfe"><span class="section-number-3">3.6.</span> Creando la nota encriptada</h3>
|
||
|
<div class="outline-text-3" id="text-3-6">
|
||
|
<p>
|
||
|
Ahora que ya tenemos el formulario, ya solo nos queda encriptar y guardar nuestra nota. Para ello comenzaremos agregando la siguiente función a nuestro modelo:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">public function encrypt(): string
|
||
|
{
|
||
|
// Generamos una llave aleatoria de entre 12 a 32 caracteres
|
||
|
$secretKey = substr(
|
||
|
str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'),
|
||
|
0,
|
||
|
mt_rand(16, 32)
|
||
|
);
|
||
|
|
||
|
// Encriptamos la nota en AES256 usando la llave antes generada
|
||
|
$this->content = openssl_encrypt(
|
||
|
$this->content,
|
||
|
'AES-256-CBC',
|
||
|
$secretKey,
|
||
|
OPENSSL_RAW_DATA,
|
||
|
substr($secretKey, 0, 16)
|
||
|
);
|
||
|
|
||
|
// Devolvemos la llave
|
||
|
return $secretKey;
|
||
|
}
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Con esto podemos encriptar la nota antes de guardarla y nos devolverá una llave aleatoria con la que habrá encriptado el texto.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Ahora vamos a hacer un controlador que recibirá el formulario, encriptará la nota para luego guardarla en la base de datos y finalmente redirigirá al enlace de la nota que llevará en la URL la llave de encriptado, para ello añadiremos esta función a la clase <code>NoteController</code>:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">public static function create(Request $request): void
|
||
|
{
|
||
|
// Creamos una instancia de Note y le pasamos los datos del formulario
|
||
|
$note = new Note;
|
||
|
$note->content = $request->post->note;
|
||
|
$note->max_views = $request->post->max_views;
|
||
|
$note->expire_at = strtotime("+{$request->post->expiration} minutes");
|
||
|
|
||
|
// Encriptamos los datos y luego los guardamos
|
||
|
$secretKey = $note->encrypt();
|
||
|
$note->save();
|
||
|
|
||
|
// Redirigimos a la url final donde se verá la nota
|
||
|
Router::redirect("/{$note->id}/{$secretKey}");
|
||
|
}
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Desde luego, también tenemos que añadir los uses de <code>Libs\Router</code> y <code>Models\Note</code>:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">use Libs\Router;
|
||
|
use Models\Note;
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Si te sientes permido y no sabes dónde poner esas 2 líneas, es arriba de la línea que comeniza con <code>class</code>, de hecho ahí verás otras líneas similares.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Para finalizar vamos configurar la ruta. Así que editamos el archivo <code>note.php</code> que está en <code>src/Routers/</code> y le añadimos:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">Router::post('/', [NoteController::class, 'create']);
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Esta vez, como la petición viene de un formulario que la enviará mediande el método HTTP POST, al momento de configurar nuestra ruta, en lugar de usar el <code>Router::get</code> que hemos estado usando hasta ahora, usamos <code>Router::post</code>.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Ahora ya podemos comenzar a crear nuestras notas, auque aún no las podremos ver.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org162e7c5" class="outline-3">
|
||
|
<h3 id="org162e7c5"><span class="section-number-3">3.7.</span> Recta final: Desencriptando y mostrando las notas</h3>
|
||
|
<div class="outline-text-3" id="text-3-7">
|
||
|
<p>
|
||
|
Vamos directo al grano, editaremos el modelo (<code>src/Models/Note.php</code>) y le añadiremos esta otra función para desencriptar:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">public function decrypt(string $secretKey) : void
|
||
|
{
|
||
|
// Verificamos si la nota ha expirado
|
||
|
if ($this->expire_at < time()) {
|
||
|
$this->content = 'La nota se ha destruido por expiración.';
|
||
|
$this->delete();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Intentamos desencriptar.
|
||
|
$content = openssl_decrypt(
|
||
|
$this->content,
|
||
|
'AES-256-CBC',
|
||
|
$secretKey,
|
||
|
OPENSSL_RAW_DATA,
|
||
|
substr($secretKey, 0, 16)
|
||
|
);
|
||
|
|
||
|
// Verificamos si desencriptó correctamente
|
||
|
if (is_string($content)) {
|
||
|
|
||
|
// Verificamos si está configurado para infinitas vistas.
|
||
|
if ($this->max_views == 0) {
|
||
|
$this->content = $content;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Reducimos el contador de vistas
|
||
|
$this->max_views = $this->max_views - 1;
|
||
|
$this->save();
|
||
|
|
||
|
// Verificamos si el contador de vistas ha llegado a 0.
|
||
|
if ($this->max_views <= 0) {
|
||
|
$this->content = 'La nota se ha destruido según su límite de vistas.';
|
||
|
$this->delete();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Si no ha expirado, solo devolvemos el contenido
|
||
|
$this->content = $content;
|
||
|
} else {
|
||
|
// Si no se pudo desencriptar colocamos un error
|
||
|
$this->content = 'Llave incorrecta.';
|
||
|
}
|
||
|
|
||
|
}
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Ahora en el <code>NoteController</code> añadiremos esta otra función:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">public static function show(Request $request): void
|
||
|
{
|
||
|
// Verificamos si el ID es un número
|
||
|
if (!is_numeric($request->params->id)) {
|
||
|
Router::defaultNotFound();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Obtenemos la nota de la base de datos
|
||
|
$note = Note::getById($request->params->id);
|
||
|
|
||
|
// Verificamos si la nota existe
|
||
|
if (is_null($note)) {
|
||
|
Router::defaultNotFound();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Desencriptamos la nota
|
||
|
$note->decrypt($request->params->key);
|
||
|
|
||
|
// Imprimimos en formato de texto plano el resultado del desencriptado
|
||
|
$view = new View;
|
||
|
$view->text($note->content);
|
||
|
}
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
En este código hay 2 cosas nuevas de Duckbrain que aún no hemos visto:
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
La primera serían los atributos <code>$request->params->id</code> y <code>$request->params->key</code> que propablemente te preguntes de dónde salen. Pero aún es momento de que te los explique, por lo que ten paciencia que no fatla mucho.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
La segunda cosa es el método <code>text</code> de la librería <code>View</code>: Este método lo que hace es imprimir en texto plano lo que le enviemos, dicho de otro modo, en lugar de devolver el usual documento html que suelen cargar todas lás páginas, simulará que estamos abriendo un archivo <code>.txt</code>.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Ahora la cerecita del paste: Vamos a configurar el router, así que editamos nuestro archivo <code>src/Views/note.php</code> y le añadimos la siguiente línea:
|
||
|
</p>
|
||
|
|
||
|
<div class="org-src-container">
|
||
|
<pre class="src src-php">Router::get('/{id}/{key}', [NoteController::class, 'show']);
|
||
|
</pre>
|
||
|
</div>
|
||
|
|
||
|
<p>
|
||
|
Ahora notarás que esta vez tiene algo singular que no hemos visto antes y es que en la ruta hay textos encerrados entre llaves. Lo que eso quiere decir es que el texto allí puede ser arbitrario y que lo que pongas allí se guardará en una variable y si ya uniste las líneas esas variables luego podremos accederlas mediante <code>$request->params->id</code> y <code>$request->params->key</code> respectivamente (llevan el mismo nombre alfinal por algo).
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Por si aún no se entiende, habamos un ejemplo:
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Supongamos que entramos mediante la url: <code>http://localhost/52/urE44g4we4r14daFifG</code> entonces el valor de <code>$request->params->id</code> sería <code>52</code>, mientras que el de <code>$request->params->key</code> sería <code>urE44g4we4r14daFifG</code>.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Si aún no lo entiendes, te recomiendo testearlo cambiando esos valores e imprimiendo los valores de esas variables en el método <code>show</code> de <code>NoteController</code>.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Y con eso ya hemos terminado, ya puedes probar la aplicación, intentar romperla, etc.
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Ya puedes comenzar a hacer tus propias aplicaciones, no tienes ninguna idea de qué hacer, te reto a hacer lo siguiente:
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Crea una aplicación de gestión tareas (un TODO list), en donde puedas agregar tareas, marcarlas como finalizadas, en espera o canceladas. Si te parece sencillo hasta ahí, añade que el sistema tendrá usuarios que deberán loguearse, cada uno tendrá sus propias listas de tareas que solo ellos pueden ver cuando se loguean.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-orgf570fd9" class="outline-3">
|
||
|
<h3 id="orgf570fd9"><span class="section-number-3">3.8.</span> Repositorio</h3>
|
||
|
<div class="outline-text-3" id="text-3-8">
|
||
|
<p>
|
||
|
Por si alguien se perdió o sencillamente quiere ver código, he subido la aplicación ignota en este repositorio:
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
[URL faltante]
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
Te animo a hacerle un fork, mejorarlo y enviármelo. Los forks que me parezcan interesantes, los colocaré como destacados en el repositorio original para que otros puedan verlos.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div id="outline-container-org94b4628" class="outline-2">
|
||
|
<h2 id="org94b4628"><span class="section-number-2">4.</span> Contacto</h2>
|
||
|
<div class="outline-text-2" id="text-4">
|
||
|
<p>
|
||
|
Como siempre, pueden contactarme mediante el correo <code>webmaster@outcontol.net</code>
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
También pueden unirse a mi comunidad de discord: <a href="https://discord.gg/p7TAAnTJPK">https://discord.gg/p7TAAnTJPK</a>
|
||
|
</p>
|
||
|
|
||
|
<p>
|
||
|
No es una comunidad de programación solamente, pero ahora mismo no existe una de solo Duckbrain.
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
<div id="postamble" class="status">
|
||
|
<p class="author">Author: KJ</p>
|
||
|
<p class="date">Created: 2024-05-13 lun 00:21</p>
|
||
|
<p class="validation"><a href="https://validator.w3.org/check?uri=referer">Validate</a></p>
|
||
|
</div>
|
||
|
</body>
|
||
|
</html>
|