Como se tornar um guru do OpenCart? [fechadas]


98

Parece que eles não têm documentação, exceto algumas chamadas de API em seus fóruns oficiais. Tenho experiência com framework Zend e framework CodeIgniter. Algum mestre em OpenCart pode me recomendar a melhor maneira de aprender e dominar no mais curto espaço de tempo? Tenho que fazer um grande projeto com ele logo.


Consulte docs.opencart.com e a visão geral da estrutura da Qphoria . Também navegue pelos tópicos em Fóruns OpenCart> Suporte . No futuro, esperamos ter um site OpenCart SE dedicado em area51.stackexchange.com/proposals/77558/opencart
Pacerier

Esta é uma questão realmente importante, às vezes superar os padrões do SO é muito útil ...
KAD

1
Muito obrigado por esta pergunta, ela me ajudou muito
Bipul Roy

@BipulRoy que bom! Parece que é uma pergunta popular agora!
CodeCrack

Respostas:


311

Guia de início rápido do desenvolvedor OpenCart 1.5.X para iniciantes

Este guia foi escrito para desenvolvedores já familiarizados com PHP, OOP e a arquitetura MVC

A seguir, você verá exemplos para o lado do catálogo do carrinho. O lado do administrador é idêntico em função, com exceção das visualizações que são indicadas na seção relevante


Noções básicas sobre bibliotecas

Todas as funcionalidades da biblioteca podem ser acessadas por meio de Controller, Model e Views usando $this->library_name. Todos eles podem ser encontrados na /system/library/pasta. Por exemplo, para acessar os produtos do carrinho de compras atual, você precisará usar a Cartclasse, que está em /system/library/cart.phpe pode ser acessada usando$this->cart->getProducts()

Itens comumente usados

  • customer.php - Funções relacionadas ao cliente
  • user.php - Funções relacionadas ao usuário administrador
  • cart.php - Funções relacionadas ao carrinho
  • config.php - Todas as configurações são carregadas a partir deste
  • url.php - funções de geração de URL

Compreender o parâmetro de rota

A estrutura do OpenCart depende do route=aaa/bbb/cccparâmetro da string de consulta para saber o que carregar e é o recurso de base para encontrar os arquivos que você precisa editar para cada página. Na verdade, a maioria das rotas usa apenas o, aaa/bbbque deve ser visto como duas partes, no entanto, algumas contêm três partes. aaa/bbb/cccA primeira parte aaageralmente está relacionada à pasta dentro de uma pasta genérica, como as pastas do controlador ou do modelo. A segunda parte geralmente está relacionada ao nome do arquivo, sem a extensão .phpou relevante .tpl. A terceira parte é explicada na seção "Compreendendo os controladores" abaixo


Compreender idiomas

Os idiomas são armazenados em uma /catalog/language/pasta na your-languagesubpasta. Dentro dele, os valores de texto gerais usados ​​em várias páginas são armazenados no your-language.phparquivo dentro da pasta, portanto, para o idioma inglês no catálogo, você encontrará os valores em catalog/language/english/english.php. Para um texto de página específico, você precisará do routepara a página (geralmente é o caso, mas nem sempre, pois você pode especificar qualquer arquivo de idioma que desejar). Por exemplo, a página de pesquisa tem a rota product/searche, portanto, o texto específico do idioma para essa página pode ser encontrado em catalog/language/english/product/search.php(Observe que o nome e a subpasta do arquivo correspondem à rota seguida por .php.

Para carregar o idioma em um controlador, você usa

$this->language->load('product/search');

Então você pode usar a função de biblioteca de linguagem getpara recuperar textos de linguagem específicos, como

$some_variable = $this->language->get('heading_title');

As variáveis ​​de idioma são atribuídas no arquivo de idioma usando uma variável especial $_que é uma matriz de chaves e valores de texto. No seu, /catalog/language/english/product/search.phpvocê deve encontrar algo semelhante a

$_['heading_title']     = 'Search';

Os valores no arquivo de idioma global english/english.phpsão carregados automaticamente e disponíveis para uso sem o $this->language->loadmétodo


Compreendendo os controladores

Os controladores são carregados com base no routee são bastante simples de entender. Os controladores estão localizados na /catalog/controller/pasta. Continuando com o último exemplo, o controlador para a página de pesquisa está /product/search.phpdentro desta pasta. Observe novamente que a rota seguida por .phpé usada.

Abrindo o arquivo do controlador, você verá um nome de Controllerclasse Pascal Case estendendo a classe, chamada ControllerProductSearch. Novamente, isso é específico da rota, Controllerseguido pelo nome da subpasta e pelo nome do arquivo sem a extensão em maiúsculas. A capitalização não é realmente necessária, mas é recomendada para facilitar a leitura. É importante notar que os nomes das classes não recebem nenhum valor da subpasta e do nome do arquivo, exceto letras e números. Os sublinhados são removidos.

Dentro da classe estão os métodos. Os métodos na classe declarada publicsão acessíveis para serem executados através da rota - privatenão são. Por padrão, com uma rota padrão de duas partes ( aaa/bbbacima), um index()método padrão é chamado. Se a terceira parte de uma rota ( cccacima) for usada, este método será executado. Por exemplo, account/return/insertirá carregar o /catalog/controller/account/return.phparquivo e a classe e tentar chamar o insertmétodo


Modelos de compreensão

Os modelos no OpenCart são encontrados na /catalog/model/pasta e são agrupados com base na função, não na rota e, portanto, você precisará carregá-los em seu controlador via

$this->load->model('xxx/yyy');

Isso carregará o arquivo na subpasta xxxchamada yyy.php. Ele fica então disponível para uso por meio do objeto

$this->model_xxx_yyy

e como acontece com os controladores, você só pode chamar seus publicmétodos. Por exemplo, para redimensionar uma imagem, você usaria o tool/imagemodelo e chamaria seu resizemétodo da seguinte maneira

$this->load->model('tool/image');
$this->model_tool_image->resize('image.png', 300, 200);

Compreendendo a atribuição de variáveis ​​em visualizações do controlador

Para passar valores do controlador para a visualização, você simplesmente precisa atribuir seus dados à $this->datavariável, que é essencialmente uma matriz de pares chave => valor. Como um exemplo

$this->data['example_var'] = 123;

Acessar isso em uma visão é um pouco fácil de entender se você estiver familiarizado com o método extract () que converte cada chave em uma variável. Portanto, a example_varchave se torna $example_vare pode ser acessada como tal na visualização.


Compreender temas

Os temas estão disponíveis apenas para o catálogo e são basicamente uma pasta de modelos, folhas de estilo e imagens de temas. As pastas do tema são colocadas na /catalog/view/theme/pasta seguida pelo nome do tema. O nome da pasta não é importante com exceção da defaultpasta

O lado do administrador usa /admin/view/template/(pulando o /theme/theme-name/do caminho, pois não permite temas diferentes)

Os arquivos de modelo residem em uma templatepasta dentro da pasta do tema. Se algum modelo não estiver disponível para o tema selecionado no momento, o modelo da pasta padrão será usado como fallback. Isso significa que os temas podem ser criados com muito poucos arquivos e ainda funcionam totalmente. Também reduz a duplicação de código e problemas à medida que as atualizações são feitas


Compreendendo as visualizações (modelos)

Assim como ocorre com a linguagem e os modelos, os arquivos de visualização geralmente estão relacionados à rota, embora não necessariamente. Os modelos no lado do catálogo geralmente são encontrados em, a /catalog/view/theme/your-theme/template/menos que não exista; nesse caso, os modelos do tema padrão serão usados. Para nosso exemplo de página de pesquisa acima, o arquivo é product/search.tpl. Para rotas com três partes, geralmente é in, aaa/bbb_ccc.tplembora não haja nenhuma regra definida. No administrador, a maioria das páginas segue isso, com a exceção de que as páginas que listam itens, como a página de lista de produtos, estão em catalog/product_list.tple o formulário de edição de produtos está em catalog/product_form.tpl. Novamente, eles não são definidos, mas um padrão para o carrinho padrão.

O arquivo de modelo é na verdade apenas outro arquivo php, mas com uma extensão .tpl e é realmente executado no arquivo do controlador, portanto, todas as coisas que você pode codificar em um controlador podem ser executadas em um arquivo de modelo (embora não seja recomendado a menos que seja absolutamente necessário)


Compreendendo o objeto de banco de dados

As consultas são executadas usando

$result = $this->db->query("SELECT * FROM `" . DB_PREFIX . "table`");

DB_PREFIX como o nome sugere, é uma constante contendo o prefixo do banco de dados, se houver

$resultretornará um objeto para SELECTconsultas, contendo algumas propriedades

$result->row contém os dados da primeira linha se um ou mais forem retornados como uma matriz associativa

$result->rows contém uma matriz de resultados de linha, ideal para repetir usando foreach

$result->num_rows contém o número de resultados retornados

Existem também alguns métodos extras que o $this->dbobjeto possui

$this->db->escape()usa mysql_real_escape_string () no valor passado

$this->db->countAffectedretorna o número de linhas afetadas por uma UPDATEconsulta e assim por diante

$this->db->getLastId()retorna o último id de incremento automático usando mysql_insert_id ()


Compreendendo as variáveis ​​reservadas

OpenCart tem predefinidos variáveis para usar no lugar do padrão $_GET, $_POST, $_SESSION, $_COOKIE, $_FILES, $_REQUESTE$_SERVER

$_SESSIONé editado usando $this->session->dataonde os dados são uma matriz associativa que imita o$_SESSION

Todos os outros podem ser acessados ​​usando $this->requeste foram "limpos" para cumprir as aspas mágicas ativadas / desativadas,

$_GET torna-se $this->request->get

$_POST torna-se $this->request->post

$_COOKIE torna-se $this->request->cookie

$_FILES torna-se $this->request->files

$_REQUEST torna-se $this->request->request

$_SERVER torna-se $this->request->server


Resumo

Embora o texto acima não seja um guia à prova de balas para desenvolvedores, espero que sirva como um bom ponto de partida para quem está começando


3
Obrigado! Onde você conseguiu isso?
CodeCrack

64
Eu escrevi. Achei que precisava ser escrito e encaixado na pergunta
Jay Gilford

2
Muito agradável. Muito mais informativo do que a maioria dos guias. Você tem um avançado por acaso?
CodeCrack

3
Deve receber o status de Community Wiki
Stann0rz

1
@UltimateKing - Este guia já fornece informações mais do que suficientes para você começar a fazer seus próprios mods. Existem também tutoriais específicos para o desenvolvimento de módulos na rede
Jay Gilford,

36

Métodos de biblioteca global: funções básicas da biblioteca opencart junto com suas funcionalidades. A maioria delas pode ser chamada de qualquer lugar no catálogo ou nas pastas de administração (controladores, modelos, visualizações)

CACHE
$this->cache->delete($key) - Deletes cache [product, category, country, zone, language, currency,
manufacturer]

CART
$this->cart->getProducts() Gets all products currently in the cart including options, discounted prices, etc.
$this->cart->add( $product_id, $qty = 1, $options = array()) - Allows you to add a product to the cart
$this->cart->remove( $key ) - Allows you to remove a product from the cart
$this->cart->clear() - Allows you to remove all products from the cart
$this->cart->getWeight() - Sum of the weight of all products in the cart that have require shipping set to Yes
$this->cart->getSubTotal() - returns the subtotal of all products added together before tax
$this->cart->getTotal() - returns the total of all products added together after tax
$this->cart->countProducts() - returns the count of all product in the cart
$this->cart->hasProducts() - returns true if there is at least one item in the cart
$this->cart->hasStock() - returns false if there is at least one item in the cart that is out of stock
$this->cart->hasShipping() - returns true if there is at least one item in the cart that requires shipping
$this->cart->hasDownload() - returns true if there is at least one item in the cart that has a download
associated

CONFIG
$this->config->get($key) - returns setting value by keyname based on application (catalog or admin)
$this->config->set($key, $value) - set the value to override the setting value. DOES NOT SAVE TO DATABASE

CURRENCY
$this->currency->set($currency) - set or override the currency code to be used in the session
$this->currency->format($number, $currency = '', $value = '', $format = TRUE) - format the currency
$this->currency->convert($value, $from, $to) - convert a value from one currency to another. Currencies must
exist
$this->currency->getId() - get the database entry id for the current currency (1, 2, 3, 4)
$this->currency->getCode() - get the 3-letter iso code for the current currency (USD, EUR, GBP, AUD, etc)
$this->currency->getValue($currency) - get the current exchange rate from the database for the specified
currency.
$this->currency->has(currency) - Check if a currency exists in the opencart currency list

CUSTOMER
$this->customer->login($email, $password) - Log a customer in
$this->customer->logout() - Log a customer out
$this->customer->isLogged() - check if customer is logged in
$this->customer->getId() - get the database entry id for the current customer (integer)
$this->customer->getFirstName() - get customer first name
$this->customer->getLastName() - get customer last name
$this->customer->getEmail() - get customer email
$this->customer->getTelephone() - get customer telephone number
$this->customer->getFax() - get customer fax number
$this->customer->getNewsletter() - get customer newsletter status
$this->customer->getCustomerGroupId() - get customer group id
$this->customer->getAddressId() - get customer default address id (maps to the address database field)

DATABASE
$this->db->query($sql) - Execute the specified sql statement. Returns row data and rowcount.
$this->db->escape($value) - Escape/clean data before entering it into database
$this->db->countAffected($sql) - Returns count of affected rows from most recent query execution
$this->db->getLastId($sql) - Returns last auto-increment id from more recent query execution 4

DOCUMENT (*Called from controller only before renderer)
$this->document->setTitle($title) - Set page title
$this->document->getTitle()- Get page title
$this->document->setDescription($description) - Set meta description
$this->document->getDescription()- Get meta description
$this->document->setKeywords()- Set meta keywords
$this->document->getKeywords()- Get meta keywords
$this->document->setBase($base) - Set page base
$this->document->getBase() - Get page base
$this->document->setCharset($charset) - Set page charset
$this->document->getCharset() - Get page charset
$this->document->setLanguage($language) - Set page language
$this->document->getLanguage()- Get page language
$this->document->setDirection($direction) - Set page direction (rtl/ltr)
$this->document->getDirection()- Get page direction (rtl/ltr)
$this->document->addLink( $href, $rel )  Add dynamic <link> tag
$this->document->getLinks()- Get page link tags
$this->document->addStyle( $href, $rel = 'stylesheet', $media = 'screen' )  Add dynamic style
$this->document->getStyles()- Get page styles
$this->document->addScript( $script ) - Add dynamic script
$this->document->getScripts()- Get page scripts
$this->document->addBreadcrumb($text, $href, $separator = ' &gt; ')  Add breadcrumb
$this->document->getBreadcrumbs()- Get Breadcrumbs

ENCRYPT
$this->encryption->encrypt($value) - Encrypt data based on key in admin settings
$this->encryption->decrypt($value) - Decrypt data based on key in admin settings

IMAGE
$this->image->resize($width = 0, $height = 0)

JSON
$this->json->encode( $data )
$this->json->decode( $data , $assoc = FALSE)

LANGUAGE
$this->language->load($filename);

LENGTH
$this->length->convert($value, $from, $to) - convert a length to another. units must exist
$this->length->format($value, $unit, $decimal_point = '.', $thousand_point = ',') - format the length to use
unit

LOG
$this->log->write($message) - Writes to the system error log

REQUEST
$this->request->clean($data) - Cleans the data coming in to prevent XSS
$this->request->get['x'] - Same as $_GET['x']
$this->request->post['x'] - Same as $_POST['x']

RESPONSE
$this->response->addHeader($header) - additional php header tags can be defined here
$this->response->redirect($url) - redirects to the url specified

TAX
$this->tax->setZone($country_id, $zone_id) - Set the country and zone id for taxing (integer)
$this->tax->calculate($value, $tax_class_id, $calculate = TRUE) - Calculate all taxes to be added to the total
$this->tax->getRate($tax_class_id) - Get the rates of a tax class id
$this->tax->getDescription($tax_class_id) - Get the description of a tax class id
$this->tax->has($tax_class_id) - Check if a tax class id exists in opencart

SESSION
$this->session->data['x'] - Same as $_SESSION['x']  

9

Existe um site OpenCart Wiki com documentação para desenvolvedores iniciantes. Siga os urls fornecidos abaixo para mais detalhes:

http://wiki.opencarthelp.com/doku.php?id=start
http://wiki.opencarthelp.com/doku.php?id=methods_reference

Links de ARQUIVO DE INTERNET

http://web.archive.org/web/20160305131349/http://wiki.opencarthelp.com/doku.php?id=start http://web.archive.org/web/20160305131349/http://wiki .opencarthelp.com / doku.php? id = methods_reference

Por exemplo, a referência do método tem detalhes para:

  1. Login do cliente
  2. Acesso DB
  3. Manuseio de carrinho de compras
  4. Config
  5. Cache
  6. Manuseio de moeda

Ainda existem algumas páginas em construção, mas vai ser útil.

[Atualizar]

Desde janeiro de 2018, o domínio opencarhelp.com está fora do ar.


Parece que ainda falta muita informação depois de alguns meses. Este projeto foi abandonado?
Pacerier

@Pacerier, não tenho certeza disso.
Dharmang

esta é uma ótima página, mesmo que o wiki do carrinho aberto faça referência a ela, você pode ver o link aqui wiki.opencarthelp.com/doku.php?id=opencart_framework
Nassim

4

Embora este tópico já tenha sido respondido muitas vezes, gostaria de oferecer outra abordagem para dominar o OpenCart com base na minha experiência.

Aprendendo fazendo

Ao criar sua própria estrutura OpenCart do zero com um punhado de arquivos, você pode entender como tudo é montado. Estarei imitando a estrutura de arquivos do OpenCart para você.

Crie um arquivo index.php

<?php
// My simpleCart

1. Registro

Opencart usa o padrão do Registro para listar todas as instâncias das classes carregadas. É o coração do seu aplicativo OpenCart. O objeto de registro é então repassado a cada categoria, modelo e biblioteca para acesso rápido a outros objetos.

crie um arquivo com caminho /system/engine/registry.php

<?php
// Registry class. 
class Registry
{
    private $data = array();

    public function set($key, $value){
        $this->data[$key] = $value;
    }

    public function get($key){
        return (isset($this->data[$key])) ? $this->data[$key] : false;
    }
}

na tua index.php

<?php
// My simpleCart

//load dependency files
require_once('system/engine/registry.php');

//initialize registry
$registry = new Registry;

2. Saída

Agora vamos adicionar uma saída que será nosso HTML no futuro. Afinal, a ideia é enviar uma string de texto para o navegador.

Criar arquivo system/library/response.php

<?php
class Response {
    private $output;

    public function getOutput() {
        return $this->output;
    }

    public function setOutput($output) {
        $this->output = $output;
    }

    public function output() {
        if ($this->output) {
            echo $this->output;
        }
    }
}

e em seu index.php

<?php
// My simpleCart

//load dependency files
require_once('system/engine/registry.php');
require_once('system/library/response.php');

//initialize registry
$registry = new Registry;

//initialize response
$response = new Response;
//add response object to the registry
$registry->set('response', $response);

//lets set an output as a test
$registry->get('response')->setOutput('Hello World');

//send the output to the client
$registry->get('response')->output();

Observe que adicionei Hello world apenas como exemplo. Vamos removê-lo mais adiante. Atualize seu site para verificar. O navegador deve ser exibido Hello World.

3. Controladores

Pense nos controladores como páginas. Eles vão definir o que será mostrado ao cliente: texto, html, json, download ou mesmo uma imagem. Por enquanto, queremos apenas uma página que envie texto.

Vamos criar um controlador para a homepágina.

adicione um arquivo com caminho catalog/controller/common/home.php

<?php

class ControllerCommonHome{

    private $registry = array();

    public function __construct($registry){
        $this->registry = $registry;
    }

    public function index(){

        $output = 'Home Page';
        //using the registry to get the response object and set the Output
        $this->registry->get('response')->setOutput($output);
    }
}

e edite o seu index.php

<?php
// My simpleCart

//load registry
require_once('system/engine/registry.php');
//load response
require_once('system/library/response.php');

//initialize registry
$registry = new Registry;

//initialize response
$response = new Response;
//add resoinse object to the registry
$registry->set('response', $response);

//load controller common/home
require_once('catalog/controller/common/home.php');
$controller = new ControllerCommonHome($registry);
$controller->index();

//send the output to the client
$registry->get('response')->output();

note como eu passei o $refistrypara o ControllerCommonHome para que pudesse acessá-lo dentro do controlador.

4. Roteador

Não queremos que os controladores sejam codificados, certo. Usaremos um parâmetro routedo endereço de url para informar ao carrinho qual controlador carregar.

Crie um arquivo com caminho system/library/request.php

<?php
class Request {
    public $get = array();

    //for now I just need the $_GET parameter
    public function __construct() {
        $this->get = $_GET;
    }
}

Crie a classe de roteador que será responsável por inicializar o arquivo do controlador com base na rota (em outras palavras: chame o controlador dinamicamente)

<?php
class Router {
    private $registry;

    public function __construct($registry) {
        $this->registry = $registry;
    }

    public function dispatch($route) {
        require_once('catalog/controller/'.$route.'.php');
        $class = "Controller".str_replace('/', '', $route);
        $controller = new $class($this->registry);
        $controller->index();
    }
}

carregue-o em seu index.php

<?php
require_once('system/engine/registry.php');
require_once('system/engine/router.php');
require_once('system/library/response.php');
require_once('system/library/request.php');

$registry = new Registry;

$response = new Response;
$registry->set('response', $response);

$request = new Request;
$registry->set('request', $request);

//get the route from the url
if(isset($registry->get('request')->get['route'])){
    $route = $registry->get('request')->get['route'];
}else{
    $route = 'common/home';
}

//initiate the router and dispatch it base on the route
$router = new Router($registry);
$router->dispatch($route);


$registry->get('response')->output();

Observe como eu carrego tudo no $registrye, em seguida, passo para o, $routerque então passa para o $controller.

Este post já é muito longo, mas espero que dê uma compreensão básica do padrão MVC no OpenCart.

Se você quiser que eu continue com esta postagem e diga como outras coisas funcionam, como modelos e visualizações, avalie esta resposta para que eu saiba.

Confira também meu https://www.youtube.com/dreamvention do Youtube e meu blog https://dreamvention.com/blog . Estarei postando mais dicas e tutoriais lá para vocês!


1

PHP é uma linguagem bastante grande com mais de 5000 funções integradas, portanto, uma estratégia para aprender uma nova plataforma é identificar quais funções ela usa com mais frequência e passar algum tempo conhecendo-as muito bem.

Eu executei algumas consultas no código-fonte do OpenCart e as 10 funções mais comumente usadas são:

array()
count()
explode()
implode()
mktime()
delete()
time()
date()
sprintf()
list()

Todos os 52 listados aqui, bem como os comandos bash do Linux que você pode usar em qualquer base de código para identificar funções comumente usadas: https://www.antropy.co.uk/blog/efficient-learning-for-new-opencart-developers/


1

Esta lista de reprodução de vídeos do youtube também pode ser útil para se tornarem desenvolvedores Gurus OpenCart:

Tutoriais de vídeos OpenCart

  1. Introdução e índice Este vídeo apresenta a introdução da série
  2. Instalação do OpenCart localhost Este vídeo mostra a instalação do OpenCart no localhost
  3. Estrutura de arquivos e pastas do Opencart Descreve a estrutura de arquivos e pastas do OpenCart
  4. Criando o esquema da tabela do banco de dados no OpenCart Mostra o esquema da tabela do banco de dados e mostra como criar tabelas do banco de dados no OpenCart
  5. Métodos de objetos predefinidos da biblioteca OpenCart Descreve os métodos de objetos predefinidos da biblioteca OpenCart e mostra onde encontrá-los.
  6. Padrão MVCL, fluxo de código e solicitação e resposta em OpenCart Mostra o padrão MVCL, fluxo de código e solicitação e resposta em OpenCart. Eles descrevem o fluxo como na imagem abaixo: MVCL descrito com Código

  7. Instalar, configurar e desinstalar o módulo Opencart Mostra três maneiras de fazer upload de módulos e, em seguida, instalar, configurar e desinstalar o módulo / extensão OpenCart 3.

  8. Layouts e posição no Opencart 3 Descreve os layouts e posições do OpenCart 3. Ele mostra como mostrar layouts personalizados para páginas diferentes, dando exemplos de páginas de categorias. Mostramos o layout diferente para uma categoria diferente.

  9. Visão geral de eventos do Opencart Você aprenderá o que são eventos no OpenCart, como funcionam e o que os torna tão úteis.

  10. Documentação da API Opencart para desenvolvedor Este vídeo mostrará como usar e fazer a API opencart personalizada

Depois de ver esses vídeos, você pode começar a codificar :)

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.